fix(ci): salvage superseded review runs and hold the loop's report-time base refresh - #10123
fix(ci): salvage superseded review runs and hold the loop's report-time base refresh#10123wenshao wants to merge 18 commits into
Conversation
…me base refresh A synchronize used to cancel-in-progress the in-flight review run no matter how close to done it was: on PR #9729 a push cancelled run 32726618419 4h06m in, minutes from posting, and cancelled review runs wasted ~7h41m of runner time across that one PR. A declarative cancel cannot weigh the run's progress, so the supersede decision moves into the run: cancel-in-progress now fires on closed only, a synchronize run queues pending in the PR-scoped group, and the in-flight run's new supersede watcher either ends the attempt early (pre-threshold: the queued replacement reviews the new head from scratch, within a poll interval) or, past the salvage threshold (compose artifact present, or elapsed >= 50% of the size-aware budget, tunable via QWEN_REVIEW_SALVAGE_ELAPSED_PERCENT), finishes and posts against the head it reviewed. The gh-wrapper write guard gains a salvage escape pinned to the reviewed head; the CLI's existing presubmit head-drift gate annotates and caps the verdict, and a new step posts a historical-head note naming the anchor the next incremental review starts from. The delay job now also skips a queued run whose event head went stale while it waited. The other half covers the one loop-owned head move outside the #8888/#8899 dispatch hold: the autofix report step's stale-base update-branch. It now probes for a live review-pr first (same probe pair as the scan gate, fail-open on probe errors) and defers the refresh with the retry branch's sentinel semantics instead of superseding the review; the deferred round joins the consecutive-failure exemptions. Rationale recorded as qwen-autofix.md#af-148, and the af-099 delay-window entry is updated for the new supersede semantics. Both workflows' .size-baseline entries are bumped in this PR per the growth ratchet: the review workflow gains the watcher/salvage machinery (+15 KB) and the autofix report step gains the probe pair (+6 KB) — real feature growth, with the long-form rationale kept in the design record. Fixes #10110
a8802c5 to
4a4afb9
Compare
|
|
|
Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration. 中文请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。 |
|
Thanks for the PR! Template looks good ✓ — minor nit: the body carries no Problem: observed, not theoretical. #10110 documents run 32726618419 cancelled 4h06m in — minutes from posting — and ~7h41m of cancelled review time across PR #9729 alone, with concrete run IDs and timestamps. The cancel sources are identified too: pushes outside the #8899 hold, including maintainer pushes that no hold can cover. Direction: aligned. This is the repo's own review automation, and the PR implements exactly the two halves the issue proposed: salvage past a progress threshold instead of declarative cancellation, and extending the existing hold to the loop's one remaining head move (the report-time Size: no core-package paths touched — the diff is Approach: the scope matches the issue's two halves, and nothing in the diff is unrelated — the af-099 doc refresh, the af-148 record, and the Risk: no elevated risk signals — none of the changed files match the revert-history high-risk path list. The change does reshape live CI concurrency semantics, so the code review will focus on failure modes. Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ —— 小瑕疵:正文没有 问题:已观测,非理论。#10110 记录了 run 32726618419 在 4h06m、距发布几分钟时被取消,仅 PR #9729 一个 PR 就累计浪费约 7h41m 被取消的评审时间,并给出具体 run ID 与时间戳;取消源也已定位——#8899 hold 覆盖不到的 push,包括任何 hold 都无法覆盖的维护者 push。 方向:对齐。这是仓库自己的评审自动化,PR 恰好实现了 issue 提出的两半:过阈值后落地而非声明式取消,以及把现有 hold 扩展到循环最后一个 head 移动(report 期 规模:未触及核心包路径——diff 为 方案:范围与 issue 的两半一致,diff 无夹带——af-099 文档更新、af-148 记录与 风险:无升级风险信号——改动文件均未命中 revert 历史高风险路径清单。本次改动重塑线上 CI 并发语义,代码审查将聚焦失败模式。 进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewFrom the issue alone, my own proposal would have been: drop
No critical findings. Two non-blocking observations:
How the supersede decision works nowsequenceDiagram
participant P1 as synchronize push
participant P2 as PR concurrency group
participant P3 as in-flight review run
participant P4 as supersede watcher
participant P5 as gh write guard
P1->>P2: queues PENDING, latest push replaces the pending slot
P4->>P4: polls live head every 60s
alt move before salvage threshold
P4->>P3: writes supersede marker, kills review tree
P3-->>P2: exits 0, queued replacement starts within a poll interval
else past threshold (compose done or half budget spent)
P4->>P4: arms salvage marker pinned to reviewed head
P3->>P5: posts the review against the reviewed head
P3->>P2: historical-head note names the next delta anchor
end
Files changed (7)
Testing evidence (the PR's own CI, read via API — PR code never executed here)CI on the reviewed commit is still settling. The PR classifies as the Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 Not verified — and why no sandboxed lane is named: the end-to-end claim (a superseded run cedes within one poll interval; a salvaged run's post actually lands against the old head) is GitHub Actions concurrency behavior, which no CI job can exercise in isolation and neither sandboxed lane covers — The PR description reports the replay suites green on the author's machine (with three pre-existing environmental failures there) — that is the author's claim, not evidence verified by this pass; the CI signal above is the evidence. 中文说明代码审查:只看 issue 时我的独立方案就是——把
无阻塞问题。两条非阻塞观察:① 残留轮询竞态——head 在下次轮询前 ≤60 秒内移动且恰好先撞上写保护时,发布仍会按现状被拦截,退化结果是"回到今天",不会更差;② scan gate 与 report 步骤之间的探测对重复是有意的镜像(af-099 惯例),两侧都有测试钉住,workflow YAML 场景下可接受。 测试证据:被评审提交的 CI 仍在进行——该 PR 分类为 full 档(github_ci_only 白名单只有三个文件),完整 ubuntu vitest 套件(含三个新工作流测试文件)正在运行;macOS/Windows 通道被平台敏感性分类器跳过(改动仅限 .github/ 与 scripts/,属宿主决定行为,已在 classify-platform-sensitivity.mjs 中核实,非基础设施故障);CLI 集成测试只在 merge_group 运行。安全检查、密钥扫描、CVE 审计与桌面壳编译通道已绿;工作流尺寸棘轮在 ubuntu job 内运行。表格区域由 finalize job 在 CI 完成后原地更新。 未验证项(以及为何不点名沙箱通道):端到端并发行为(被顶替 run 在一个轮询间隔内让位、salvage 发布真正落在旧 head 上)无法被任何 CI job 单独演练,两个沙箱通道也都不覆盖——/verify 做的是产品构建 A/B,/tmux 驱动 TUI,都不执行工作流 YAML。回放套件钉住的是提取出的决策逻辑,剩余缺口是观察性的:合并后第一次真实顶替即验证,且所有已识别的失手都只退化到现状。PR 描述中"回放套件在作者机器上绿(含三个既有环境性失败)"是作者陈述,不是本次核实的证据。 — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 4/5 — clean review of maintainer-owned automation; the only reservations are things the diff cannot answer: end-to-end concurrency behavior proves itself at the first real supersede, plus the ≤60s residual polling race. Reflection, honestly: My independent proposal going in was exactly the four moves this PR makes — scope the cancel to The question I spent the most time on is whether the failure modes stay safe, since this reshapes live CI concurrency. They do: a watcher kill degrades to the status-quo cancel-equivalent, a missed poll degrades to today's blocked post, a probe error degrades to the pre-#10110 Every change in the diff is necessary: the two halves are the issue itself, and the doc/baseline hunks are mechanical. The replay-test approach — extracting the real bash from the workflow and driving it with the #9729 incident numbers — is the right evidence class for this kind of change; the remaining gap is observational and cannot be exercised by CI or either sandboxed lane. Non-blocking nits: the PR body has no Approval is deferred until CI lands green on 中文说明置信度 4/5 —— 维护者自有自动化的干净评审;唯一的保留是 diff 回答不了的部分:端到端并发行为要等第一次真实顶替来证明,另有 ≤60 秒的残留轮询竞态。 反思:我的独立方案恰是本 PR 的四步——把 cancel 限定为 花最多时间的问题是失败模式是否安全(本次改动重塑线上并发语义)。答案是肯定的:watcher 的 kill 退化为现状等价的取消、轮询失手退化为今天的拦截发布、探测出错退化为 #10110 之前的 update-branch(fail-open,见 af-148)、推迟轮次受 MAX_ROUNDS 约束——没有任何失败比现状更糟,棘轮只朝一个方向转。 diff 中每项改动都是必要的:两个半区即 issue 本身,文档/基线为机械结果。回放式测试(提取工作流真实 bash、用 #9729 事故数字驱动)是这类改动的正确证据类别;剩余缺口是观察性的,CI 与沙箱通道均无法演练。 非阻塞小项:正文缺 审批推迟到上述提交的 CI 全绿——ubuntu 单元套件(含三个新回放套件)仍在运行;全部检查绿后由 finalize 步骤发布与该提交绑定的批准,任何检查变红或 head 移动都会收回。 — Qwen Code · qwen3.8-max Reviewed at |
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
The fleet-shepherd contract test requires every autofix HEADLINE to be named terminal or transient; the report step's new deferred-stale-base headline (#10110) was unclassified. It is transient — the loop retries on the next scan — and the shepherd's REASON regex already does not match it, so only the classification lists needed the new needle.
|
@qwen-code /takeover |
|
🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. Remove the 中文说明🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted; the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
中文说明
仅完成部分审查,审查缺口已披露。
未审查:反向审计——在 5 轮的反审轮数上限内未收敛。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted; the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
— qwen3.8-max via Qwen Code /review (v0.22.0)
| # The trap must reap the watcher on EVERY exit path (fail() included): | ||
| # a background watcher outliving this step on the reused self-hosted | ||
| # runner could later kill a NEXT job's review of the same PR. | ||
| trap 'rm -f "$LOG_PATH"; [ -z "$PROXY_BIN" ] || rm -rf "$PROXY_BIN"; [ -z "${WATCHER_PID:-}" ] || kill "${WATCHER_PID}" 2>/dev/null; [ -z "${SALVAGE_DIR:-}" ] || rm -rf "${SALVAGE_DIR}"' EXIT |
There was a problem hiding this comment.
[Critical] The new EXIT trap's watcher-reap clause is fatal under this step's set -euo pipefail. Once salvage is armed the watcher returns immediately (it arms the marker and exits), so by the time the "Salvage was armed but the attempt still died" branch takes its deliberate silent exit 0, bash has already reaped the watcher subshell. kill "${WATCHER_PID}" then fails with ESRCH, the [ -z ... ] || kill list returns non-zero, errexit aborts the trap before rm -rf "${SALVAGE_DIR}", and the step exits 1 instead of 0. The result on every salvage-armed cede: the review-pr job reports FAILURE on a deliberately clean exit — contradicting this exit path's own comment ("Ending clean here keeps the failure fallbacks silent") and the trap's "EVERY exit path" guarantee — and a per-event SALVAGE_DIR leaks into RUNNER_TEMP on the shared self-hosted runner. The supersede-yield path and every fail() path with an already-dead watcher leak the directory too. scripts/tests/qwen-pr-review-workflow.test.js:4010 string-pins the kill clause as written, so update that assertion with the fix.
Witness (bash 5.2, trap line extracted verbatim, two arms):
pre-check: kill FAILED rc=1 (reaped -> ESRCH)
arm 1 — trap AS WRITTEN: step exit-status=1, SALVAGE_DIR LEAKED (rm -rf skipped)
arm 2 — trap with `kill ... || true`: step exit-status=0, SALVAGE_DIR removed
| trap 'rm -f "$LOG_PATH"; [ -z "$PROXY_BIN" ] || rm -rf "$PROXY_BIN"; [ -z "${WATCHER_PID:-}" ] || kill "${WATCHER_PID}" 2>/dev/null; [ -z "${SALVAGE_DIR:-}" ] || rm -rf "${SALVAGE_DIR}"' EXIT | |
| trap 'rm -f "$LOG_PATH"; [ -z "$PROXY_BIN" ] || rm -rf "$PROXY_BIN"; [ -z "${WATCHER_PID:-}" ] || kill "${WATCHER_PID}" 2>/dev/null || true; [ -z "${SALVAGE_DIR:-}" ] || rm -rf "${SALVAGE_DIR}"' EXIT |
中文说明
[Critical] 新增 EXIT trap 中的 watcher 回收子句在本步骤的 set -euo pipefail 下是致命的。salvage 一旦布防,watcher 会立即返回(布防标记文件后即退出),因此当「Salvage was armed but the attempt still died」分支执行其刻意设计的静默 exit 0 时,bash 早已回收了 watcher 子 shell。此时 kill "${WATCHER_PID}" 以 ESRCH 失败,[ -z ... ] || kill 列表返回非零,errexit 在 rm -rf "${SALVAGE_DIR}" 之前中止 trap,步骤以 1 而非 0 退出。结果是每一次 salvage 布防后的让位(cede)都会:让 review-pr job 在一个刻意干净的退出上报告 FAILURE——与该退出路径自己的注释("Ending clean here keeps the failure fallbacks silent")以及 trap 自身 "EVERY exit path" 的保证相矛盾——并且在共享自托管 runner 上每次事件都会向 RUNNER_TEMP 泄漏一个 SALVAGE_DIR。supersede 让位路径以及所有在 watcher 已死时执行的 fail() 路径同样会泄漏该目录。scripts/tests/qwen-pr-review-workflow.test.js:4010 按现状逐字钉住了 kill 子句,修复时需同步更新该断言。
验证证据(bash 5.2,逐字提取 trap 行,双臂实验):按现状执行时步骤退出码为 1 且 SALVAGE_DIR 泄漏;加上 kill ... || true 后退出码为 0 且目录被清理。
— qwen3.8-max via Qwen Code /review (v0.22.0)
| if [ ! -f "${SALVAGE_DIR}/compose-seen" ] && [ -e "$COMPOSED_ARTIFACT" ]; then | ||
| : > "${SALVAGE_DIR}/compose-seen" | ||
| fi |
There was a problem hiding this comment.
[Suggestion] The compose-seen latch is written once per run but its meaning ("compose done ⇒ posting is minutes out") is per-attempt: the retry loop re-runs the review fresh from scratch and resets nothing, so attempt 2 inherits attempt 1's latch and is treated as salvage-eligible from its first second. If attempt 1 reaches compose, then dies in the posting window with a retryable outcome, and the head moves early in attempt 2 (under the 50% threshold), salvage_eligible returns true off the stale latch — the run keeps running instead of ceding, the queued replacement waits out attempt 2's entire budget, and the run ends by posting a historical-head review in the exact state the threshold policy says to CEDE. The CLI-side consumer of this same artifact (newestArtifactSince in packages/cli/src/commands/review/run.ts) mtime-fences exactly this stale-artifact class; this latch is the one consumer that does not. A probe driving the real extracted watcher confirmed the flip: with a stale latch the watcher KEEPs and arms the marker; with a fresh latch it CEDEs.
Suggested fix (reset the latch per attempt, immediately before each run_review_once inside the retry loop):
rm -f "${SALVAGE_DIR}/compose-seen"中文说明
[Suggestion] compose-seen 闩锁按 run 写入一次,但其含义("已完成 compose ⇒ 距离发布只差几分钟")是按 attempt 计的:重试循环会从零全新重跑评审且不重置任何状态,因此 attempt 2 会继承 attempt 1 的闩锁,从第一秒起就被视为可 salvage。若 attempt 1 到达 compose 后在发布窗口内以 retryable 结果死掉,且 head 在 attempt 2 早期(低于 50% 阈值)移动,salvage_eligible 会凭陈旧闩锁返回 true——run 继续运行而不是让位,排队的接替 run 要等掉 attempt 2 的整个预算,最终以历史 head 评审的形式发布——恰是阈值策略规定应当 CEDE 的状态。CLI 侧同一工件的消费者(packages/cli/src/commands/review/run.ts 中的 newestArtifactSince)用 mtime 栅栏精确防住了这类陈旧工件;此闩锁是唯一没有这样做的消费者。用真实提取的 watcher 做的探针实验确认了翻转:陈旧闩锁下 watcher KEEP 并布防标记;新闩锁下 CEDE。
建议修复:在重试循环内每次 run_review_once 之前重置闩锁(rm -f "${SALVAGE_DIR}/compose-seen")。
— qwen3.8-max via Qwen Code /review (v0.22.0)
| if [ -f "${SUPERSEDE_FILE:-}" ]; then | ||
| echo "Superseded early: PR #${PR_NUMBER:-} moved from ${EXPECTED_HEAD_SHA:-} to $(cat "$SUPERSEDE_FILE" 2>/dev/null || echo unknown) before the salvage threshold; ceding to the replacement run (#10110)." | tee -a "$GITHUB_STEP_SUMMARY" | ||
| exit 0 | ||
| fi |
There was a problem hiding this comment.
[Suggestion] SUPERSEDE_FILE has exactly one conditional read site — here, after run_review_once returns. The watcher's cede is one-shot: on a below-threshold head move it writes the file, runs pkill, and returns 0 forever. During the 60s retry backoff (or any qwen-down window) the pkill matches nothing and nothing re-checks the marker before the next attempt. Concrete shape: attempt 1 ends retryable, the author pushes during the 60s RETRY_BACKOFF_SECONDS sleep — the exact scenario #10110 exists for — the watcher cedes against an empty process table and exits, and the loop starts attempt 2 without re-checking: it re-reviews the dead head for the entire remaining budget (hours on a 360-minute tier) while the queued replacement holds the concurrency slot, reintroducing exactly the "replacement waits out the full budget" latency this PR removes. The outcome self-heals (the guard blocks any stale post; the post-attempt check exits 0), so this is a wasted-runner-hours plus delayed-replacement cost, not a wrong post — but the log line "ending this review so the queued replacement covers the new head" is false in this window. A replay of the real retry loop flips with the fix: attempts spent drops from 2 to 1 with an immediate cede after the backoff.
Suggested fix: re-check the marker at the top of the while :; loop body before run_review_once (covers the backoff and any qwen-down window), and add a replay case whose stub qwen creates the file after attempt 1, asserting r.attempts stays 1.
中文说明
[Suggestion] SUPERSEDE_FILE 只有一个条件读取点——就是这里,在 run_review_once 返回之后。watcher 的让位是一次性的:低于阈值的 head 移动发生时它写入该文件、执行 pkill,然后永远 return 0。在 60 秒重试退避期间(或任何 qwen 未运行的窗口)pkill 匹配不到任何进程,且没有东西在下一次 attempt 之前重新检查该标记。具体形态:attempt 1 以 retryable 结束,作者在 60 秒 RETRY_BACKOFF_SECONDS 睡眠期间推送——正是 #10110 要解决的场景——watcher 对空进程表让位并退出,循环未重新检查就启动 attempt 2:在剩余的全部预算(360 分钟档位数小时)里重新评审死掉的 head,而排队的接替 run 一直占着并发槽,恰好重新引入了本 PR 要消除的"接替者等满整个预算"的延迟。结果会自愈(写保护阻止陈旧发布;attempt 后检查以 0 退出),因此代价是浪费的 runner 小时数加延迟的接替,而不是错误发布——但该窗口内日志行 "ending this review so the queued replacement covers the new head" 是不成立的。对真实重试循环的重放在加入修复后翻转:attempts 从 2 降为 1,退避后立即让位。
建议修复:在 while :; 循环体顶部、run_review_once 之前重新检查该标记(覆盖退避及任何 qwen 未运行窗口),并新增一个 replay 用例:stub qwen 在 attempt 1 之后创建该文件,断言 r.attempts 保持为 1。
— qwen3.8-max via Qwen Code /review (v0.22.0)
| salvage_eligible() { | ||
| local elapsed="$1" budget="$2" compose_seen="$3" pct="$4" | ||
| if [ "$compose_seen" = "true" ]; then | ||
| return 0 | ||
| fi | ||
| [ "$(( elapsed * 100 ))" -ge "$(( budget * pct ))" ] | ||
| } |
There was a problem hiding this comment.
[Suggestion] The elapsed-fraction KEEP term is per-run, not per-attempt: elapsed counts from START_TS (set once per run), but every retry restarts the review from scratch — so after a failed attempt the fraction can clear the threshold while the current attempt has minutes of progress, contradicting the threshold's own stated rationale ("a 90-minute-in run of the same budget restarts cheaply on the new head"). Concrete shape on a 360-minute budget: attempt 1 fails retryable at ~170 min, attempt 2 starts from zero, the head moves at ~185 min (51% > 50%): with no compose-seen involved, salvage_eligible returns true off the fraction alone, attempt 2 re-reviews the stale head, posts the historical-head review, and the queued replacement waits out the remaining budget — a KEEP in the exact state the rationale says to CEDE. Executed check: eligible(11100, 21600, false, 50) = true (15 min into attempt 2) vs eligible(900, 21600, false, 50) = false for the same progress with no prior attempt. Distinct from the compose-seen latch finding — resetting that latch does not fix this.
Suggested fix: baseline eligibility per attempt — record an attempt-start timestamp when incrementing attempt and pass attempt-relative elapsed (the run-level self-bound can keep START_TS) — or drop the fraction-only KEEP for attempts > 1 and keep only the compose-seen term.
中文说明
[Suggestion] elapsed 比例 KEEP 项是按 run 计的,不是按 attempt 计的:elapsed 从 START_TS(每 run 设置一次)起算,但每次重试都从零全新重跑评审——因此一次失败 attempt 之后,比例可能越过阈值,而当前 attempt 实际只有几分钟的进展,与阈值自身声明的理由("同预算下 90 分钟的 run 可以在新 head 上廉价重启")相矛盾。以 360 分钟预算为例:attempt 1 在约 170 分钟时以 retryable 失败,attempt 2 从零开始,head 在约 185 分钟时移动(51% > 50%):在 compose-seen 未参与的情况下,salvage_eligible 仅凭比例返回 true,attempt 2 继续重新评审陈旧 head、发布历史 head 评审,排队的接替者等掉剩余预算——恰是理由中说应当 CEDE 的状态下做了 KEEP。实际执行验证:eligible(11100, 21600, false, 50) = true(attempt 2 进行 15 分钟)对比无先前 attempt 时相同进展 eligible(900, 21600, false, 50) = false。与 compose-seen 闩锁的发现相互独立——重置那个闩锁并不能修复此问题。
建议修复:按 attempt 设定基线——在递增 attempt 时记录 attempt 起始时间戳并传入相对该 attempt 的 elapsed(run 级自限可继续使用 START_TS)——或者对 attempt > 1 丢弃仅凭比例的 KEEP,只保留 compose-seen 项。
— qwen3.8-max via Qwen Code /review (v0.22.0)
| SALVAGE_ELAPSED_PERCENT="${SALVAGE_ELAPSED_PERCENT_VAR:-}" | ||
| case "$SALVAGE_ELAPSED_PERCENT" in | ||
| ''|*[!0-9]*) SALVAGE_ELAPSED_PERCENT=50 ;; | ||
| esac |
There was a problem hiding this comment.
[Suggestion] The numeric guard accepts leading-zero values that salvage_eligible's $(( budget * pct )) then parses as octal. QWEN_REVIEW_SALVAGE_ELAPSED_PERCENT=08 (or 09) passes the whole sanitize — the case pattern sees all digits and [ 08 -gt 100 ] reads decimal — and at the first head move the arithmetic raises "value too great for base", killing the background watcher subshell silently: the run then has no salvage/cede at all, so a below-threshold push no longer yields (full budget burned, queued replacement waits it out — the exact latency #10110 removes) and a past-threshold move never arms the marker (the finished run's post dies at the guard's exit 90). 050 never errors but evaluates as octal 40, silently applying a 40% threshold. Probe on the extracted code: VAR=08 → watcher exit code 1 with no KEEP/CEDE decision; VAR=050 → KEEP where the decimal control gives CEDE. The parse is never executed by any test (it sits outside retryLoopSource()'s window — see the companion finding on the test side), so nothing catches either shape.
| SALVAGE_ELAPSED_PERCENT="${SALVAGE_ELAPSED_PERCENT_VAR:-}" | |
| case "$SALVAGE_ELAPSED_PERCENT" in | |
| ''|*[!0-9]*) SALVAGE_ELAPSED_PERCENT=50 ;; | |
| esac | |
| SALVAGE_ELAPSED_PERCENT="${SALVAGE_ELAPSED_PERCENT_VAR:-}" | |
| case "$SALVAGE_ELAPSED_PERCENT" in | |
| ''|*[!0-9]*) SALVAGE_ELAPSED_PERCENT=50 ;; | |
| esac | |
| SALVAGE_ELAPSED_PERCENT=$((10#$SALVAGE_ELAPSED_PERCENT)) |
中文说明
[Suggestion] 数值守卫接受带前导零的值,而 salvage_eligible 的 $(( budget * pct )) 随后将其按八进制解析。QWEN_REVIEW_SALVAGE_ELAPSED_PERCENT=08(或 09)能通过整个清洗——case 模式看到的是纯数字,[ 08 -gt 100 ] 按十进制读取——而在第一次 head 移动时算术展开报 "value too great for base",静默杀死后台 watcher 子 shell:该 run 从此完全没有 salvage/cede,低于阈值的推送不再让位(烧掉全部预算,排队的接替者等到底——恰是 #10110 要消除的延迟),超过阈值的移动也永远不会布防标记(完成的 run 发布时在守卫处以 exit 90 死掉)。050 不会报错但按八进制 40 求值,静默套用 40% 阈值。对提取代码的探针:VAR=08 → watcher 退出码 1,无任何 KEEP/CEDE 决策;VAR=050 → 在十进制对照组给出 CEDE 处给出 KEEP。该解析逻辑没有任何测试执行到(它位于 retryLoopSource() 提取窗口之外——见测试侧的配套发现),因此两种形态都不会被发现。
建议修复(在守卫后强制十进制):SALVAGE_ELAPSED_PERCENT=$((10#$SALVAGE_ELAPSED_PERCENT))。
— qwen3.8-max via Qwen Code /review (v0.22.0)
| expect(delay.env.EVENT_HEAD_SHA).toBe( | ||
| '${{ github.event.pull_request.head.sha }}', | ||
| ); | ||
| expect(delay.run).toContain('while this run queued'); |
There was a problem hiding this comment.
[Suggestion] The delay-job stale-head skip — a core new gate of this PR — is pinned only by substring containment; the new 3-field TSV read and the comparison direction have no behavioural test. The diff changed both sides of an adjacent pairing: --jq '[.state, .isDraft, .headRefOid] | @tsv' and read -r state is_draft current_head. Both one-keystroke mutations ship green (measured, 185/185 passing under each): swapping the jq array order against the read list — current_head then receives true/false, always differs from EVENT_HEAD_SHA, and every queued lifecycle run skips itself after the delay, silently ending all automatic reviews — and flipping != to = — same-head runs skip and stale-head runs proceed to review a superseded head. The containment assertions ('while this run queued', 'should_review=false') pass under either.
Suggested fix: replay the comparison with stub env — current_head == EVENT_HEAD_SHA (assert no skip) and != (assert skip), including a case asserting the TSV field order maps headRefOid into current_head (a printf 'OPEN\tfalse\tsha-x' input through the verbatim read line).
中文说明
[Suggestion] delay job 的陈旧 head 跳过——本 PR 的核心新门禁——只被包含性断言钉住;新的三字段 TSV 读取与比较方向没有行为测试。diff 同时改动了相邻配对的两边:--jq '[.state, .isDraft, .headRefOid] | @tsv' 与 read -r state is_draft current_head。两个单键位变异都能绿着上线(已实测,各自 185/185 通过):把 jq 数组顺序与 read 列表错位——current_head 将接收到 true/false,永远与 EVENT_HEAD_SHA 不同,每个排队的生命周期 run 在延迟后都会跳过自己,静默终结所有自动评审;以及把 != 翻成 =——head 相同的 run 被跳过、陈旧 head 的 run 继续评审已被取代的 head。包含性断言('while this run queued'、'should_review=false')在两种变异下都通过。
建议修复:用 stub env 重放该比较——current_head == EVENT_HEAD_SHA(断言不跳过)与 !=(断言跳过),并包含一个断言 TSV 字段顺序把 headRefOid 映射进 current_head 的用例(以 printf 'OPEN\tfalse\tsha-x' 输入走逐字的 read 行)。
— qwen3.8-max via Qwen Code /review (v0.22.0)
There was a problem hiding this comment.
Deferred to the next round (this round's batch cap was ~8 findings; the Critical plus the salvage-logic correctness fixes and their witnesses went first). The finding is accepted as stated — the TSV field order and the != direction need a behavioral replay, not just containment — and it is queued as the first test-side item next round.
中文说明
推迟到下一轮(本轮批量上限约 8 项发现;Critical 加 salvage 逻辑的正确性修复及其见证优先)。该发现按原文接受 —— TSV 字段顺序与 != 方向需要行为重放,而非仅包含性断言 —— 已列为下一轮首个测试侧事项。
| expect(r.raw).toContain('Superseded early:'); | ||
| expect(r.raw).toContain('ceding to the replacement run (#10110)'); | ||
| expect(r.raw).not.toContain('FAIL '); |
There was a problem hiding this comment.
[Suggestion] The two salvage-cede early exits depend on exiting 0, but runScenario swallows a non-zero exit (catch (e) { stdout = e.stdout ?? '' }, no status returned) and both cede tests assert only transcript content. Exit 0 is load-bearing by the workflow's own comments: "Ending clean here keeps the failure fallbacks silent", and the fallback-comment gate is needs.review-pr.result == 'failure' — which the diff's concurrency comment claims the superseded step "never open[s]". A future edit turning either exit 0 into exit 1 ships green through both new tests (measured mutant: both exits flipped — all three replayed-loop tests still passed), and every healthy supersede then turns the review-pr job red and opens the fallback gate, whose paths then rely solely on head-moved guards that prefer posting over silence when the comparison API is unavailable. This gap is also exactly what would let the EXIT-trap exit-0→exit-1 flip (the Critical above) ship uncaught.
Suggested fix: capture the exit status in runScenario (e.g. exitCode = e.status ?? 0 in the catch, 0 on success, and return it), then assert expect(r.exitCode).toBe(0) in both cede scenarios.
中文说明
[Suggestion] 两处 salvage 让位的提前退出依赖以 0 退出,但 runScenario 吞掉了非零退出码(catch (e) { stdout = e.stdout ?? '' },不返回状态),两个让位测试也只断言输出内容。按工作流自己的注释,退出 0 是承重设计:"Ending clean here keeps the failure fallbacks silent",且 fallback-comment 的门是 needs.review-pr.result == 'failure'——diff 的并发注释声称被取代的步骤"永远不会打开"它。未来把任一 exit 0 改成 exit 1 的编辑能在两个新测试下绿着上线(已实测变异:两处退出翻转后三个重放循环测试仍全部通过),于是每次健康的取代都会把 review-pr job 变红并打开 fallback 门,其后各路径只能依赖 head 移动守卫——当比较 API 不可用时它们宁可发布也不沉默。这个缺口也正是上面那个 EXIT trap 退出码 0→1 翻转能不被发现就上线的原因。
建议修复:在 runScenario 中捕获退出状态(如 catch 中 exitCode = e.status ?? 0、成功时为 0 并返回),然后在两个让位场景断言 expect(r.exitCode).toBe(0)。
— qwen3.8-max via Qwen Code /review (v0.22.0)
There was a problem hiding this comment.
Deferred to the next round (this round's batch cap was ~8 findings; the Critical plus the salvage-logic correctness fixes and their witnesses went first). Note that the Critical's exit-0 semantics are already behaviorally witnessed this round by the new trap replay (reaps an already-exited watcher without failing the clean cede asserts step exit status 0); capturing runScenario's exit status and asserting it in the two cede scenarios remains queued for next round.
中文说明
推迟到下一轮(本轮批量上限约 8 项发现;Critical 加 salvage 逻辑的正确性修复及其见证优先)。注意 Critical 的退出 0 语义本轮已由新的 trap 重放行为见证(reaps an already-exited watcher without failing the clean cede 断言步骤退出码为 0);在 runScenario 中捕获退出码并在两个让位场景中断言,仍排在下一轮。
| expect(env.SALVAGE_ELAPSED_PERCENT_VAR).toBe( | ||
| '${{ vars.QWEN_REVIEW_SALVAGE_ELAPSED_PERCENT }}', | ||
| ); | ||
| expect(run).toContain('SALVAGE_ELAPSED_PERCENT=50'); |
There was a problem hiding this comment.
[Suggestion] The new QWEN_REVIEW_SALVAGE_ELAPSED_PERCENT parsing — the case guard ''|*[!0-9]* and the -gt 100 clamp at workflow :1355-1361 — is pinned only by this substring containment and is never executed by any test: it sits before retryLoopSource()'s extraction window (which starts at OUTCOME=''), so the replayed loop never sees it. Measured mutant: inverting the guard glob (''|*[!0-9]* → ''|*[0-9]*) leaves this pin matching and the test passing — numeric settings (e.g. 30) would then match the pattern and be silently reset to 50 (the documented tunable is inert), while non-numeric values would pass through and kill the watcher subshell with an unbound-variable expansion error at the first salvage_eligible call. The adjacent clamp is equally unpinned (flip -gt to -lt and a setting of 30 becomes 100). Executed parse on the real block: '30' → 50 (tunable silently inert), '150' → 50 (reset before the clamp, so the clamp is unreachable).
Suggested fix: extract the parse the same way salvage_eligible is extracted and replay it under stub env — '30' → 30, '' → 50, 'abc' → 50, '150' → 100, '08' → 8 (see the octal finding); at minimum pin the case-pattern line and the clamp line verbatim.
中文说明
[Suggestion] 新增的 QWEN_REVIEW_SALVAGE_ELAPSED_PERCENT 解析——工作流 :1355-1361 的 case 守卫 ''|*[!0-9]* 与 -gt 100 钳制——只被这一包含性断言钉住,没有任何测试执行过它:它位于 retryLoopSource() 提取窗口(从 OUTCOME='' 开始)之前,重放的循环根本看不到它。实测变异:反转守卫 glob(''|*[!0-9]* → ''|*[0-9]*)后该断言依旧匹配、测试依旧通过——数值设置(如 30)将匹配该模式并被静默重置为 50(文档承诺的可调参数失效),非数值则会穿透并在第一次 salvage_eligible 调用时以未绑定变量展开错误杀死 watcher 子 shell。相邻的钳制同样未被钉住(把 -gt 翻成 -lt,设置 30 就变成 100)。对真实代码块执行解析:'30' → 50(可调参数静默失效)、'150' → 50(先被重置,钳制不可达)。
建议修复:像提取 salvage_eligible 一样提取该解析并在 stub env 下重放——'30' → 30、'' → 50、'abc' → 50、'150' → 100、'08' → 8(见八进制发现);至少逐字钉住 case 模式行与钳制行。
— qwen3.8-max via Qwen Code /review (v0.22.0)
| const parked = { | ||
| event: 'pull_request_target', | ||
| status: 'waiting', | ||
| head_sha: 'sha-live', | ||
| pull_requests: [], | ||
| }; |
There was a problem hiding this comment.
[Suggestion] This runs-API fallback replay exercises only status: 'waiting' (plus one completed negative); its acceptance of queued/pending/requested/in_progress — the statuses a replacement lifecycle run holds while parked in the PR concurrency group, the central case of #10110 — is pinned by nothing, and the rollup-layer replay likewise tests only 2 of the 5 pending-ish check-run statuses. The scan-gate mirror this block claims parity with loops over all five statuses in this same file. Measured mutants: narrowing the report-step runs filter to IN("waiting") and the rollup filter to IN("QUEUED", "IN_PROGRESS") both ship green against these 5 tests, while driving the extracted filters through real jq shows the probe distinguishes all five. Production shape: in the handover window the outgoing run has completed (rollup layer blind — no check-run yet) and the parked replacement is queued — a narrowed filter reads "no review live", fires update-branch, and supersedes the replacement before it starts: the exact race this hold was added to prevent (self-healing via the new delay-job stale-head guard, so the cost is a silently no-op hold plus a wasted delay window, not a wrong post).
Suggested fix: mirror the scan gate's shape — extract the runs filter once and loop expect(probe([{ ...parked, status: s }])).toBe('true') over all five pending-ish statuses (keep the negatives); extend the rollup replay the same way over QUEUED/IN_PROGRESS/PENDING/WAITING/REQUESTED.
中文说明
[Suggestion] 这个 runs-API 兜底重放只测试了 status: 'waiting'(外加一个 completed 反例);它对 queued/pending/requested/in_progress——接替的生命周期 run 停在 PR 并发组中时所持的状态,即 #10110 的核心场景——没有任何钉住,rollup 层重放同样只测了 5 种待处理 check-run 状态中的 2 种。本块声称与之等价的 scan 门禁镜像在同一文件中对全部五种状态做了循环。实测变异:把 report 步骤的 runs 过滤器收窄为 IN("waiting")、把 rollup 过滤器收窄为 IN("QUEUED", "IN_PROGRESS"),这 5 个测试都绿着通过;而用真实 jq 驱动提取出的过滤器表明探针本可区分全部五种。生产形态:在交接窗口中离任 run 已完成(rollup 层失明——还没有 check-run),停放的接替者为 queued——收窄后的过滤器读出"无评审在飞",触发 update-branch,在接替者启动前就取代了它:正是这个 hold 要防止的竞态(新加的 delay job 陈旧 head 守卫使其自愈,因此代价是静默失效的 hold 加浪费的延迟窗口,而非错误发布)。
建议修复:对齐 scan 门禁的形态——提取一次 runs 过滤器并对全部五种待处理状态循环 expect(probe([{ ...parked, status: s }])).toBe('true')(保留反例);rollup 重放同样扩展到 QUEUED/IN_PROGRESS/PENDING/WAITING/REQUESTED。
— qwen3.8-max via Qwen Code /review (v0.22.0)
There was a problem hiding this comment.
Deferred to the next round (this round's batch cap was ~8 findings; the Critical plus the salvage-logic correctness fixes and their witnesses went first). The finding is accepted as stated — loop the runs filter over all five pending-ish statuses (and the rollup over QUEUED/IN_PROGRESS/PENDING/WAITING/REQUESTED), mirroring the scan gate's shape.
中文说明
推迟到下一轮(本轮批量上限约 8 项发现;Critical 加 salvage 逻辑的正确性修复及其见证优先)。该发现按原文接受 —— 对齐 scan 门禁的形态,让 runs 过滤器对全部五种待处理状态循环(rollup 对 QUEUED/IN_PROGRESS/PENDING/WAITING/REQUESTED 同样扩展)。
| # live, the next round re-runs, and ITS report step performs | ||
| # the base refresh once the review has landed. | ||
| MARK_TS='9999-12-31T23:59:59Z' | ||
| HEADLINE="🤖 AutoFix deferred a stale-base refresh — the fix did not pass verification and this PR is behind \`${DEFAULT_BRANCH:-main}\`, but a review of this PR is still in flight, and merging main now would cancel it or invalidate its posting (#10110). The refresh and this item's retry move to the next scan." |
There was a problem hiding this comment.
[Suggestion] The deferred headline this diff adds says the avoided merge "would cancel it or invalidate its posting (#10110)" — the pre-#10110 framing, and false for exactly the case this PR exists for: a past-threshold lifecycle review salvages and posts its historical-head review, neither cancelled nor invalidated, and the claim directly contradicts the "⏳ Historical-head review" note the salvage path posts into the same comment stream. Three unchanged strings carry the same newly-stale wording: the hold log at :2652 ("holding this round so the push cannot cancel it (#8888)"), the user-facing ack-on-defer comment at :2683 ("a push now would cancel it and discard its work" / "现在推送会取消该 review 并丢弃其工作"), and qwen-autofix.md#af-028's present-tense claim that head mutations cancel "via qwen-code-pr-review.yml's cancel-in-progress" — which reads as making the #8888 gate pointless now that the cancel is gone. Post-merge, a push supersedes in-run: only a parked/pre-threshold run yields. The deferral behavior itself is correct; only the wording is wrong.
Suggested fix: reword all four to the new semantics, e.g. "holding this round so the push cannot supersede it (#8888/#10110)" and "merging main now would supersede it — an early run loses its work, a late run is forced to salvage mid-flight (#10110)", matching the updated af-099 text.
中文说明
[Suggestion] 本 diff 新增的延迟标题称被避免的合并"会取消它或使其发布失效(#10110)"——这是 #10110 之前的表述,而且对本 PR 要解决的核心场景恰好是错的:超阈值的生命周期评审会 salvage 并发布其历史 head 评审,既未被取消也未失效,该说法与 salvage 路径发布到同一评论流的"⏳ Historical-head review"说明直接矛盾。另有三处未改动的字符串带有同样新近过时的表述::2652 的 hold 日志("holding this round so the push cannot cancel it (#8888)")、:2683 面向用户的延迟确认评论("a push now would cancel it and discard its work" / "现在推送会取消该 review 并丢弃其工作"),以及 qwen-autofix.md#af-028 中"head 变更会通过 qwen-code-pr-review.yml 的 cancel-in-progress 取消在飞评审"的现在时表述——在取消已移除后读起来像是在说 #8888 门禁毫无意义。合并后,推送触发 run 内的取代决策:只有停放/未达阈值的 run 会让位。延迟行为本身是正确的;只是措辞错了。
建议修复:把四处全部改为新语义,例如"holding this round so the push cannot supersede it (#8888/#10110)"与"merging main now would supersede it — an early run loses its work, a late run is forced to salvage mid-flight (#10110)",与更新后的 af-099 文本一致。
— qwen3.8-max via Qwen Code /review (v0.22.0)
There was a problem hiding this comment.
Deferred to the next round (this round's batch cap was ~8 findings; the Critical plus the salvage-logic correctness fixes and their witnesses went first). The wording finding is accepted — all four spots (the new deferred headline, the hold log at ~:2652, the ack-on-defer comment at ~:2683, and qwen-autofix.md#af-028) will be reworded to the post-#10110 supersede semantics together next round.
中文说明
推迟到下一轮(本轮批量上限约 8 项发现;Critical 加 salvage 逻辑的正确性修复及其见证优先)。措辞发现已接受 —— 四处(新增的延迟标题、约 :2652 的 hold 日志、约 :2683 的延迟确认评论、qwen-autofix.md#af-028)将在下一轮一并改为 #10110 之后的取代语义。
Round 1 review findings on the salvage machinery. The EXIT trap's watcher reap killed an already-reaped subshell on the salvage-cede path: under errexit the failed kill aborted the trap before the SALVAGE_DIR cleanup and turned the deliberate clean exit into exit 1 — the kill now tolerates a reaped watcher. The salvage threshold parse accepts leading-zero values that the watcher arithmetic reads as octal (`08` kills the first salvage_eligible call, `050` silently applies 40%); the value is coerced decimal after the digit guard. Salvage eligibility is now per attempt, matching the threshold's own rationale: the retry loop restarts the review from zero, so attempt 1's compose sighting is unlatched and its elapsed fraction no longer counts against attempt 2 — the loop resets the latch and records an attempt-start timestamp the watcher reads (self-bound stays per run). The supersede file is also re-checked at the top of the loop: a cede landing during the retry backoff (pkill matched nothing — qwen not running) used to be noticed only after a full re-review of the dead head. Docs-only runs always cede on a moved head: their only posting vehicle skips any moved head and the salvage-note step excludes them, so an armed KEEP could never post. The watcher's KEEP/CEDE dispatch is now replayed with stub gh/pkill/sleep (marker carries the reviewed head, supersede file the live one, docs-only and fresh-attempt cedes pinned), the parse and the trap are replayed too, and runScenario gains the backoff-cede and latch-reset scenarios. Six mutation probes confirm each guard is witnessed; all eight changed tests fail against the pre-round workflow. The .size-baseline entry is bumped per the growth ratchet.
|
🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下: Round 1 review feedback — PR #10123Batch cap applied (~8 findings per round): the Critical plus seven Decisions
Also refreshed: the Conflict notesNone — Verification
中文说明第 1 轮评审反馈 — PR #10123应用了批量上限(每轮约 8 项发现):本轮实现 Critical 加七项正确性/见证发现;其余四项 Suggestion 明确推迟到下一轮(已在各自线程回复并保持打开)。 处理决定
另外刷新: 冲突说明无 — 验证
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: reverse audit — stopped before round 7 by the review time budget.
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
中文说明
仅完成部分审查,审查缺口已披露。
未审查:反向审计——评审时间预算不足,未能开始第 7 轮。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| rm -f "${SALVAGE_DIR}/compose-seen" | ||
| date +%s > "${SALVAGE_DIR}/attempt-start" |
There was a problem hiding this comment.
[Critical] R1-2: (fix-induced) The per-attempt reset added this round for R1-2 deletes only the compose-seen latch, not the composed artifact that drives it. When attempt 1 reaches compose and then dies retryably (an API error during submit/report — exactly what run_review_once classifies retryable), the skill's Step 9 cleanup never runs, so .qwen/tmp/qwen-review-pr-<n>-composed.json survives; within one poll (≤60s) the watcher's [ ! -f compose-seen ] && [ -e "$COMPOSED_ARTIFACT" ] re-latches from it. If the head then moves at any point during attempt 2, compose_seen=true makes salvage_eligible return KEEP regardless of the fresh attempt's near-zero elapsed fraction — the run re-reviews the stale head for the entire remaining budget (hours on a large PR) while the queued replacement that owns the live head waits, then posts a historical-head review the threshold policy says should never have been armed: the exact waste #10110 exists to prevent, surviving in the retry corner. The compose_latch_reset test cannot catch this because it never seeds the artifact.
Witness (probe over the extracted watcher at HEAD, stale artifact seeded, head moved, attempt age 0s):
after reset: compose_seen=no, artifact=present
qwen-review-salvage: head moved to head-b 0s in (compose_seen=true) — finishing and posting against reviewed head head-a
marker=head-a superseded=ABSENT pkilled=no ← a zero-second attempt gets KEEP
with rm -f "$COMPOSED_ARTIFACT" added to the reset the probe flips: artifact=gone → superseded=head-b, marker=ABSENT, pkilled=yes (CEDE).
| rm -f "${SALVAGE_DIR}/compose-seen" | |
| date +%s > "${SALVAGE_DIR}/attempt-start" | |
| rm -f "${SALVAGE_DIR}/compose-seen" "$COMPOSED_ARTIFACT" | |
| date +%s > "${SALVAGE_DIR}/attempt-start" |
(Alternatively gate the latch on artifact mtime ≥ attempt-start, mirroring the CLI's newestArtifactSince.)
Fix witness: add a replayed-watcher/loop case that seeds a stale $COMPOSED_ARTIFACT, runs the per-attempt reset, then drives one watcher poll with a moved head and a fresh below-threshold attempt-start, asserting CEDE (superseded set, marker null) — removing the artifact removal must turn it red (today's code arms the marker).
中文说明
[Critical] R1-2:(由修复引入)本轮针对 R1-2 新增的按尝试重置只删除了 compose-seen 锁存文件,没有删除驱动该锁存的 compose 工件。当尝试 1 完成 compose 后以可重试方式失败(提交/上报阶段的 API 错误——正是 run_review_once 归类为可重试的情形),skill 的 Step 9 清理不会执行,.qwen/tmp/qwen-review-pr-<n>-composed.json 残留;观察者在一次轮询(≤60 秒)内就会通过 [ ! -f compose-seen ] && [ -e "$COMPOSED_ARTIFACT" ] 从残留工件重新锁存。此后尝试 2 期间任何时刻 head 移动,compose_seen=true 都会让 salvage_eligible 无视新尝试接近零的已耗时返回 KEEP——整个剩余预算(大 PR 上以小时计)被用来重审过期的 head,而拥有新 head 的排队接替 run 只能等待,最终发布一条阈值策略本不允许的历史 head 评审:正是 #10110 要消除的浪费,在重试角落存活了下来。compose_latch_reset 测试抓不到这一点,因为它从不放置工件文件。
验证证据(对 HEAD 提取的 watcher 做探针:放置残留工件、head 移动、尝试年龄 0 秒):按现状执行时 marker=head-a、superseded=ABSENT、pkilled=no(0 秒的尝试得到 KEEP);在重置中加入 rm -f "$COMPOSED_ARTIFACT" 后翻转为 superseded=head-b、marker=ABSENT、pkilled=yes(CEDE)。
修复:在重置处一并删除工件(上方 suggestion),或将锁存条件改为工件 mtime ≥ attempt-start(与 CLI 的 newestArtifactSince 一致)。
修复验证:新增一个 watcher/loop 回放用例——放置残留 $COMPOSED_ARTIFACT、执行按尝试重置、在 head 移动且尝试耗时远低于阈值时驱动一次轮询,断言 CEDE(写出 superseded、无 marker);删除工件清理后该测试必须变红(现状代码会布防 marker)。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| # ${VAR:-} defaults: the retry-loop replay in | ||
| # scripts/tests/qwen-pr-review-workflow.test.js extracts this loop | ||
| # verbatim and runs it under set -u without the salvage setup. | ||
| if [ -f "${SUPERSEDE_FILE:-}" ]; then |
There was a problem hiding this comment.
[Critical] R2-1: Both supersede-cede checks trust SUPERSEDE_FILE unconditionally, but the reviewed agent can derive its path from its own inherited environment — QWEN_CI_REVIEW_SALVAGE_OK_FILE="${SALVAGE_DIR}/salvage-ok" is exported, and SALVAGE_DIR is its dirname — and write the file itself. A prompt-injected agent (yolo, no-sandbox, over attacker-controlled PR content) that writes $(dirname "$QWEN_CI_REVIEW_SALVAGE_OK_FILE")/superseded" gets a clean silent suppression of the entire review: after the attempt — whatever its outcome, even success — this check exits 0, no review is posted, the failure-fallback gate stays closed on the clean exit, and since the head never actually moved there is no queued replacement run. The PR simply receives no automatic review while the job shows green. The diff's threat-model comment bounds only marker forgery ("only lets it post to an old head"); supersede forgery is a distinct capability that rationale never argues.
Witness (probe against the verbatim extracted loop):
forgery arm: exit=0, attempts=1, summary 'Superseded early: … ceding to the replacement run' — green, nothing posted
no-forgery arm: exit=1, attempts=2, FAIL reason=[Qwen review completed but produced no output.] — the machinery the forgery silences
Suggested fix — verify the premise before ceding (the watcher only writes the file on a real head move, so the legitimate path is unaffected):
current="$(gh pr view "$PR_NUMBER" --repo "$REPO" --json headRefOid --jq '.headRefOid' 2>/dev/null || true)"
if [ -n "$current" ] && [ "$current" != "$EXPECTED_HEAD_SHA" ]; then
# …existing cede body…
fiFix witness: a replay case that plants SUPERSEDE_FILE while stubbing gh to report the head UNCHANGED must assert the loop does NOT exit early; removing the head verification makes it red. The existing supersede scenarios need the moved-head stub to stay green.
中文说明
[Critical] R2-1:两处 supersede 让位检查都无条件信任 SUPERSEDE_FILE,但被评审的 agent 可以从自己继承的环境推导出该路径——QWEN_CI_REVIEW_SALVAGE_OK_FILE="${SALVAGE_DIR}/salvage-ok" 已导出,SALVAGE_DIR 就是它的 dirname——并自行写入该文件。被提示注入的 agent(yolo、无沙箱、处理攻击者可控的 PR 内容)写入 superseded 文件后,即可干净且无声地压制整次评审:无论尝试结果如何(即使成功),该检查都会 exit 0,不发布任何评审,失败兜底门因干净退出保持关闭,而 head 实际上从未移动、不存在排队的接替 run——PR 在 job 显示绿色的情况下得不到任何自动评审。diff 的威胁模型注释只界定了 marker 伪造("只是允许它向旧 head 发布");supersede 伪造是该论证从未覆盖的另一项能力。
验证证据(对逐字提取的 loop 做探针):伪造臂 exit=0、attempts=1,输出 'Superseded early … ceding',绿色、无任何发布;无伪造对照臂 exit=1、attempts=2、FAIL——伪造所压制的机制本身。
建议修复:让位前先验证前提——重新读取 live head,仅当非空且不等于 $EXPECTED_HEAD_SHA 时才让位(watcher 只在 head 真实移动时写该文件,合法路径不受影响)。
修复验证:新增回放用例——放置 SUPERSEDE_FILE 同时 stub gh 报告 head 未移动,断言 loop 不会提前退出;删除 head 校验后该测试必须变红。现有 supersede 场景需改用 moved-head stub 才能保持绿色。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| if [ ! -f "${SALVAGE_DIR}/compose-seen" ] && [ -e "$COMPOSED_ARTIFACT" ]; then | ||
| : > "${SALVAGE_DIR}/compose-seen" | ||
| fi |
There was a problem hiding this comment.
[Critical] R2-2: The compose latch treats a bare -e existence check on .qwen/tmp/qwen-review-pr-<N>-composed.json as a trusted control signal, but that workspace file is creatable by the reviewed agent itself — the path is derivable from cwd plus the PR number, and the agent runs yolo/no-sandbox. A prompt-injected agent that touches it forces KEEP and disables the supersede cede: on the next poll the watcher latches compose-seen; when a maintainer later pushes, salvage_eligible returns KEEP regardless of the attempt's near-zero elapsed fraction, the run burns the remaining budget re-reviewing the stale head, and the queued replacement waits it out. Since each replacement run's agent re-reads the same PR content, the injection re-fires per run — a persistent, PR-content-steerable multi-hour waste. This is distinct from mere agent stalling: a stalled agent's run gets killed within one poll of a push; the forged latch is precisely what defeats that. The marker file's trust model is at least explicitly documented in guard_pr_write ("a correctness gate, not a security boundary"); the latch drives the kill/cede decision and has no such treatment.
Witness (probe, identical run state — attempt 30s old, deep CEDE territory):
control: compose-seen latched: no superseded: head-b pkilled: yes ← CEDE
forged file: compose-seen latched: yes marker: head-a pkilled: no ← KEEP
hardened latch: superseded: head-b pkilled: yes ← flips back
Suggested fix: do not latch on bare existence — validate the artifact before latching (parse it as composed-verdict JSON and require the expected shape), and/or gate on mtime ≥ attempt-start (which also closes the R1-2 staleness hole); document the residual forgery risk the way the marker escape does.
Fix witness: a replayed-watcher case asserting a bogus/empty planted artifact does NOT latch (the watcher still cedes below threshold); removing the validation makes it red. No test currently creates $COMPOSED_ARTIFACT for the watcher to discover.
中文说明
[Critical] R2-2:compose 锁存把对工作区文件 .qwen/tmp/qwen-review-pr-<N>-composed.json 的裸 -e 存在性检查当作可信控制信号,但该文件可被评审 agent 自己创建——路径可由 cwd + PR 编号推导,且 agent 以 yolo/无沙箱运行。被提示注入的 agent touch 该文件即可强制 KEEP、废掉 supersede 让位:下次轮询观察者锁存 compose-seen;之后维护者 push 时,无论尝试耗时多短 salvage_eligible 都返回 KEEP,run 烧掉剩余预算重审过期 head,排队的接替 run 只能干等。每个接替 run 的 agent 都会重新读取同一份 PR 内容,注入因此逐 run 复燃——这是可由 PR 内容操控的持续性多小时浪费。它与 agent 单纯拖延不同:拖延的 run 会在 push 后一次轮询内被终止;伪造的锁存恰恰废掉了这一机制。marker 文件的信任模型至少在 guard_pr_write 中有明确文档("正确性闸门,不是安全边界");锁存驱动的是 kill/cede 决策,却没有同等待遇。
验证证据(探针,相同运行状态——尝试仅 30 秒,深度 CEDE 区间):对照臂正常 CEDE;伪造文件臂转为 KEEP(marker 布防、不 kill);加固后的锁存恢复 CEDE。
建议修复:不要基于裸存在性锁存——锁存前校验工件(按 composed-verdict JSON 解析并要求预期形状),和/或要求 mtime ≥ attempt-start(同时堵住 R1-2 的陈旧工件漏洞);像 marker 逃生口那样文档化残余伪造风险。
修复验证:新增 watcher 回放用例,断言伪造/空的工件不会触发锁存(低于阈值时仍让位);删除校验后必须变红。目前没有任何测试为观察者放置 $COMPOSED_ARTIFACT。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| echo "Superseded early: PR #${PR_NUMBER:-} moved from ${EXPECTED_HEAD_SHA:-} to $(cat "$SUPERSEDE_FILE" 2>/dev/null || echo unknown) before the salvage threshold; ceding to the replacement run (#10110)." | tee -a "$GITHUB_STEP_SUMMARY" | ||
| exit 0 | ||
| fi | ||
| if [ "$OUTCOME" != "success" ] && [ -f "${QWEN_CI_REVIEW_SALVAGE_OK_FILE:-}" ]; then |
There was a problem hiding this comment.
[Critical] R2-3: The salvage-armed cede branch trusts bare existence of the salvage marker — a file whose path is exported directly into the reviewed agent's environment — with no check that the head actually moved and no content comparison (the guard escape at least requires content = expected_head). A forged marker plus a deliberately failed attempt exits the whole review clean: no review posted, the failure-fallback gate stays closed on the exit 0, and because the head never moved there is no replacement run — silent green suppression. Second arm: forged marker plus a successful attempt emits salvaged=true/salvage_moved_to=unknown and the historical-head note step posts a public note claiming the head moved to unknown for a head that never moved.
Witness (probe, verbatim extracted retry loop, stub qwen = the agent):
forge_fail: exit=0 attempts=1 | 'head already moved — ceding to the replacement run'
forge_success: exit=0 attempts=1 | GITHUB_OUTPUT: salvaged=true / salvage_moved_to=unknown
control_noforge_fail: exit=1 attempts=2 | FAIL
mutant (branch removed): forge_fail flips to exit=1 attempts=2 | FAIL
Suggested fix: verify the claim instead of the file — before ceding, re-read the live head (gh pr view "$PR_NUMBER" --repo "$REPO" --json headRefOid --jq '.headRefOid') and cede only when it is non-empty and differs from $EXPECTED_HEAD_SHA; otherwise fall through to the normal retry/fail path. (A content check alone would not fix the class — QWEN_CI_REVIEW_EXPECTED_HEAD_SHA is exported too, so the agent can forge correct content; only a live read at decision time distinguishes.)
Fix witness: the existing 'cedes instead of retrying when salvage armed but the attempt died' pins existence-only behaviour and must gain a stubbed moved-head gh; a companion case (marker present, live head unchanged → loop retries/FAILs instead of ceding) must go red if the live-head recheck is removed.
中文说明
[Critical] R2-3:salvage 布防后的让位分支仅凭存在性信任 salvage marker——其路径已直接导出到被评审 agent 的环境中——既不检查 head 是否真的移动,也不比较内容(guard 逃生口至少要求内容等于 expected_head)。伪造 marker + 故意失败的尝试会让整次评审干净退出:不发布评审、失败兜底门因 exit 0 保持关闭、且 head 从未移动故不存在接替 run——无声的绿色压制。第二臂:伪造 marker + 成功尝试会写出 salvaged=true/salvage_moved_to=unknown,历史 head 说明步骤随即为从未移动的 head 发布 "moved to unknown" 的公开说明。
验证证据(探针,逐字提取的 retry loop,stub qwen 模拟 agent):伪造+失败臂 exit=0 且让位;伪造+成功臂输出 salvaged=true/salvage_moved_to=unknown;无伪造对照臂 exit=1 进入重试;删除该分支后伪造臂翻转为 exit=1。
建议修复:验证事实而非文件——让位前重读 live head,仅当非空且不等于 $EXPECTED_HEAD_SHA 时让位,否则回到正常的重试/失败路径。(仅加内容校验不够——QWEN_CI_REVIEW_EXPECTED_HEAD_SHA 同样被导出,agent 可以伪造正确内容;只有决策时刻的实时读取才能区分。)
修复验证:现有 'cedes instead of retrying when salvage armed but the attempt died' 钉住的是纯存在性行为,需改用 moved-head 的 gh stub;补充对应用例(marker 存在但 live head 未变 → loop 重试/FAIL 而非让位),删除 live-head 校验后必须变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| if [ -f "$QWEN_CI_REVIEW_SALVAGE_OK_FILE" ]; then | ||
| { | ||
| echo "salvaged=true" | ||
| echo "salvage_moved_to=$(cat "${SALVAGE_DIR}/moved-to" 2>/dev/null || echo unknown)" |
There was a problem hiding this comment.
[Critical] R2-4: salvage_moved_to echoes the raw content of the agent-writable ${SALVAGE_DIR}/moved-to file into $GITHUB_OUTPUT; embedded newlines survive the command substitution and land as separate name=value lines the runner parses (last write wins), injecting arbitrary step outputs. This bypasses the docs-only relay's allowlist and flips the gates of downstream bot-posting steps. The file's own /resolve job rejects CR/LF in write_output precisely against this class; this block omits that defence for an agent-writable file.
Concrete chain (probe-verified): the agent seeds a multi-line moved-to plus the marker and completes normally → the block injects docs_only_medium=true, completion_line=Review complete: pr-<N> — Request changes, not posted (5 Critical, 2 Suggestion) and a forged expected_head_sha; review_completed=true is genuinely written afterwards, so the relay gate opens, its stale-head re-check passes against the forged anchor, and it posts a bot comment under CI_BOT_PAT quoting the forged verdict line (the step's allowlist only sanitizes the value it derives from RESULT_TEXT itself, which the full-run path never writes). The injected docs_only_medium=true simultaneously suppresses the historical-head note that would expose the forgery, and the forged anchor propagates to the badge/fallback consumers.
Witness (probe A — verbatim block, seeded multi-line moved-to): $GITHUB_OUTPUT gains docs_only_medium=true, completion_line=…Request changes…, expected_head_sha=forgedsha123 after the step's own writes; a CR/LF-rejecting variant flips to salvage_moved_to=unknown with no injected lines. Probe B (verbatim relay step + real upsert-bot-comment.sh, stub gh): forged outputs post the comment quoting the forged line, including after a head move against the forged anchor; the control (real anchor, moved head) skips.
Suggested fix — the watcher only ever writes a 40-hex SHA, so validate before the output write (spans the block, hence not a one-click suggestion):
moved_to="$(cat "${SALVAGE_DIR}/moved-to" 2>/dev/null || echo unknown)"
case "$moved_to" in
*[!0-9a-f]* | '' | ????????????????????????????????????????) moved_to="unknown" ;;
esac
{
echo "salvaged=true"
echo "salvage_moved_to=$moved_to"
} >> "$GITHUB_OUTPUT"Fix witness: a scenario seeding moved-to with an embedded newline and asserting the harness's $GITHUB_OUTPUT gains no line beyond salvaged=true/salvage_moved_to=unknown — no existing test runs this post-loop block at all.
中文说明
[Critical] R2-4:salvage_moved_to 把 agent 可写的 ${SALVAGE_DIR}/moved-to 文件原样回显进 $GITHUB_OUTPUT;内嵌换行在命令替换中存活,被 runner 逐行解析为 name=value(后写覆盖先写),从而注入任意步骤输出,绕过 docs-only 中转步骤的白名单并翻转下游 bot 发布步骤的门控。同文件的 /resolve job 正是为了防御这一类而在 write_output 中拒绝 CR/LF;此块却对 agent 可写文件省略了该防御。
具体链路(探针验证):agent 放置多行 moved-to 与 marker 后正常完成 → 该块注入 docs_only_medium=true、伪造的 completion_line 与 expected_head_sha;随后 review_completed=true 真实写出,中转门开启,其 stale-head 复查在伪造锚点上通过,步骤以 CI_BOT_PAT 发布引用伪造结论行的 bot 评论(该步骤的白名单只清洗它自己从 RESULT_TEXT 派生的值,而完整 run 路径从不写该值)。注入的 docs_only_medium=true 同时压制了本会暴露伪造的历史 head 说明,伪造锚点还会传播到 badge/fallback 消费者。
验证证据:探针 A(逐字提取的块 + 多行 moved-to):$GITHUB_OUTPUT 在步骤自身写入之后新增三条注入行;CR/LF 拒绝变体翻转为 salvage_moved_to=unknown 且无注入。探针 B(逐字中转步骤 + 真实 upsert 脚本):伪造输出确实发布了引用伪造结论行的评论(含 head 移动 + 伪造锚点臂);对照臂(真实锚点、head 已移动)正确跳过。
建议修复:watcher 只会写入 40 位十六进制 SHA,因此在写输出前校验(跨越多行,故不用一键 suggestion):非法形状一律归为 unknown。
修复验证:新增场景——放置内嵌换行的 moved-to,断言 $GITHUB_OUTPUT 除 salvaged=true/salvage_moved_to=unknown 外无任何新增行;目前没有任何测试执行该 loop 后块。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| if [ ! -f "${SALVAGE_DIR}/compose-seen" ] && [ -e "$COMPOSED_ARTIFACT" ]; then | ||
| : > "${SALVAGE_DIR}/compose-seen" | ||
| fi | ||
| live_head="$(gh pr view "$PR_NUMBER" --repo "$REPO" --json headRefOid --jq '.headRefOid' 2>/dev/null)" || continue |
There was a problem hiding this comment.
[Suggestion] R2-8: The watcher's || continue tolerance of a failed gh pr view poll is never exercised — the runWatcher gh stub always succeeds, and grep '|| continue' over the test file returns nothing. Under the step's set -euo pipefail, dropping || continue ships green — mutation ran: the mutant passes the single file (192/192) and the full test:scripts lane — yet in production one transient gh failure (rate limit, network blip) among hundreds of polls over a multi-hour review would errexit-kill the background watcher with no output. Any head move after that goes undetected until guard_pr_write at posting time — exit 90, hours of review discarded, i.e. the #10110 regression reinstated by one API hiccup, with no log signal.
Suggested fix: add a runWatcher variant whose gh stub fails once and then returns head-b, asserting the watcher still writes its cede/salvage signal on the later poll.
Fix witness: that case — remove || continue and the harness dies on the first failed poll before writing either signal file, so the signal assertions go red. Verified against HEAD: the variant passes on the shipped code (superseded='head-b', marker=null, pkilled=true) and fails on the mutant.
中文说明
[Suggestion] R2-8:观察者对 gh pr view 轮询失败的 || continue 容错从未被测试覆盖——runWatcher 的 gh stub 总是成功,测试文件中没有任何针对它的断言。在步骤的 set -euo pipefail 下,删除 || continue 可以绿色通过——已做变异实验:变异体通过单文件(192/192)与完整 test:scripts 泳道——但生产环境中,多小时评审的数百次轮询里只要一次瞬时 gh 失败(限流、网络抖动)就会让后台观察者因 errexit 无声死亡。此后任何 head 移动都要等到发布时 guard_pr_write 才被发现——exit 90、数小时评审被丢弃,即一次 API 抖动就复现 #10110 要修复的回归,且毫无日志信号。
建议修复:新增 runWatcher 变体——gh stub 先失败一次再返回 head-b,断言观察者仍能在后续轮询写出让位/salvage 信号。
修复验证:即该用例——删除 || continue 后 harness 会在第一次失败轮询时死亡、写不出任何信号文件,信号断言变红。已在 HEAD 上验证:该变体在现状代码上通过(superseded='head-b', marker=null, pkilled=true),在变异体上失败。
— qwen3.8-max via Qwen Code /review (v0.22.2)
There was a problem hiding this comment.
Deferred to the next round — this round's batch bound went to the five Criticals and their witnesses first. The finding stands as reported: runWatcher's gh stub always succeeds, and the mutation check (dropping || continue ships green) is convincing. Planned fix is exactly the suggested variant: a runWatcher run whose gh stub fails once and then returns head-b, asserting the watcher still writes its cede/salvage signal on the later poll.
推迟到下一轮——本轮批次上限优先安排了五条 Critical 及其验证。该发现成立:runWatcher 的 gh stub 总是成功,且变异实验(删除 || continue 可绿色通过)有说服力。计划按建议实现:gh stub 先失败一次再返回 head-b,断言观察者仍能在后续轮询写出让位/salvage 信号。
| 403688 qwen-autofix.yml | ||
| 7061 qwen-ci-flaky-rerun.yml | ||
| 158010 qwen-code-pr-review.yml | ||
| 176219 qwen-code-pr-review.yml |
There was a problem hiding this comment.
[Suggestion] R2-9: The baseline entry this PR bumps records 176219 bytes for qwen-code-pr-review.yml, but the workflow shipped in the same commit is 176231 bytes (git show HEAD:.github/workflows/qwen-code-pr-review.yml | wc -c) — the ratchet's reviewed line is wrong the moment it merges. The sibling entry this diff bumps (qwen-autofix.yml, 403688) matches exactly, showing the mismatch is a measure-before-final-edits slip, not convention. No lane goes red — the ratchet only objects past base + GROWTH_ALLOWANCE (4096), verified by running bash .github/scripts/check-workflow-size.sh (green, no warning) — so 12 bytes of the allowance are consumed unreviewed and every future tolerance for this file is off by 12 until someone notices.
| 176219 qwen-code-pr-review.yml | |
| 176231 qwen-code-pr-review.yml |
中文说明
[Suggestion] R2-9:本 PR 更新的基线条目记录 qwen-code-pr-review.yml 为 176219 字节,但同一提交中的实际文件是 176231 字节——棘轮的"已审行"在合入瞬间就是错的。同 diff 更新的另一条(qwen-autofix.yml,403688)完全吻合,说明这是最终编辑前测量的疏漏而非惯例。没有泳道会变红——棘轮只在超过 base + GROWTH_ALLOWANCE(4096)时才反对,已运行 bash .github/scripts/check-workflow-size.sh 验证(绿色、无警告)——因此 12 字节的余量被未经审查地消耗,之后该文件的所有容差都偏差 12,直到有人发现。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| if [ -n "${QWEN_CI_REVIEW_SALVAGE_OK_FILE:-}" ] \ | ||
| && [ "$(cat "${QWEN_CI_REVIEW_SALVAGE_OK_FILE}" 2>/dev/null)" = "$expected_head" ]; then |
There was a problem hiding this comment.
[Suggestion] R2-10: The salvage escape applies to every run, but the marker it trusts is only ever legitimately armed by the supersede watcher, which is gated on AUTO_REVIEW=true. In explicit runs (/review comments, review_requested, workflow_dispatch) QWEN_CI_REVIEW_SALVAGE_OK_FILE is still exported while nothing legitimate ever creates the file — so there the escape can fire only via agent forgery, re-opening a stale-head write the pre-change block refused. The in-comment rationale ("forging it only lets it post to an old head — nothing it could not already post to the live one") does not hold in the moved-head state it governs: the guard blocks every write there, so without the marker the agent cannot post at all — forgery is what restores posting. Witness (probe, real wrapper extracted verbatim, expected head-a / live head-b): moved_no_marker: exit=90; moved_forged_marker: exit=0 (PR write allowed (salvage)); pre-change reconstruction with forged marker: exit=90; closed-PR + forged marker: exit=90 (OPEN check holds). The CLI's classifyHeadDrift gate caps and annotates the verdict, which bounds the residual harm — hence Suggestion, not Critical (one uncapped window remains: a push landing between presubmit's head sample and submit).
Suggested fix — arm the escape only where salvage can legitimately exist:
if [ "${AUTO_REVIEW:-false}" = "true" ]; then
SALVAGE_DIR="$(mktemp -d …)"
export QWEN_CI_REVIEW_SALVAGE_OK_FILE="${SALVAGE_DIR}/salvage-ok"
SUPERSEDE_FILE="${SALVAGE_DIR}/superseded"
fi(the retry-loop and output reads already use ${…:-} defaults; the escape's [ -n "${QWEN_CI_REVIEW_SALVAGE_OK_FILE:-}" ] test is then naturally false in explicit runs and the stale-head block keeps its pre-change force.)
Fix witness: a wrapper-guard replay (the resolve suite's harness already replays this wrapper): with a moved head and a pre-created marker whose content equals the expected head, a non-auto run must still exit 90; it passes today only because no test exercises the escape against a forged marker.
中文说明
[Suggestion] R2-10:salvage 逃生口适用于所有 run,但它信任的 marker 只会由受 AUTO_REVIEW=true 门控的 supersede 观察者合法布防。在显式运行(/review 评论、review_requested、workflow_dispatch)中 QWEN_CI_REVIEW_SALVAGE_OK_FILE 仍然被导出,却没有任何合法途径创建该文件——因此在这些运行里逃生口只能经由 agent 伪造触发,重新打开了改动前写入块所拒绝的过期 head 写入。注释中的论证("伪造它只是允许向旧 head 发布——并没有超出它本来就能向新 head 发布的能力")在其管辖的 head 已移动状态下不成立:那里守卫阻断一切写入,没有 marker 时 agent 根本不能发布——伪造恰恰恢复了发布能力。验证证据(探针,逐字提取的真实 wrapper):移动+无 marker → exit 90;移动+伪造 marker → exit 0(允许写入);改动前形态+伪造 marker → exit 90;已关闭 PR+伪造 marker → exit 90(OPEN 检查仍有效)。CLI 的 classifyHeadDrift 门会封顶并标注 verdict,限制了残余危害——因此为 Suggestion 而非 Critical(仍存在一个未封顶窗口:push 恰好落在 presubmit 采样与 submit 之间)。
建议修复:只在 salvage 可能合法存在的运行中布防逃生口——把 SALVAGE_DIR/导出/SUPERSEDE_FILE 包进 AUTO_REVIEW 门(重试循环与输出读取已用 ${…:-} 默认值;显式运行中逃生口的 [ -n … ] 测试自然为假,过期 head 写入块保持改动前的效力)。
修复验证:wrapper 守卫回放(resolve 套件的 harness 已在回放该 wrapper)——head 已移动且预放置内容等于 expected head 的 marker 时,非自动运行仍须 exit 90;今天能通过仅因为没有测试用伪造 marker 验证逃生口。
— qwen3.8-max via Qwen Code /review (v0.22.2)
There was a problem hiding this comment.
Deferred to the next round — this round's batch bound went to the five Criticals and their witnesses first. The finding stands: in explicit runs the escape can fire only via forgery, and gating the SALVAGE_DIR setup/export on AUTO_REVIEW=true is the right shape. One correction for the implementer, verified against the current code: the post-loop output read is if [ -f "$QWEN_CI_REVIEW_SALVAGE_OK_FILE" ] — it does NOT currently carry a :- default (the finding's premise says it does), so that default must be added together with the gate or explicit runs die on set -u. The retry-loop reads already use :- and need no change. This round's live-head gates are independent of it and stay as-is.
推迟到下一轮——本轮批次上限优先安排了五条 Critical 及其验证。该发现成立:显式运行中逃生口只能经伪造触发,把 SALVAGE_DIR 的创建/导出收进 AUTO_REVIEW=true 门是正确的形状。给实施者的一处更正(已对现状代码核实):loop 后的输出读取是 if [ -f "$QWEN_CI_REVIEW_SALVAGE_OK_FILE" ],目前并没有 :- 默认值(发现中的前提描述与此不符),加门时必须一并补上该默认值,否则显式运行会死于 set -u。重试循环内的读取已用 :-,无需改动。本轮的 live-head 闸门与之相互独立,保持现状。
| # newer push may have superseded its event head. Its successor run | ||
| # owns that newer head; skip here, before review-pr spends runner | ||
| # setup to reach the same conclusion at its own stale-head guard. | ||
| if [ -n "$EVENT_HEAD_SHA" ] && [ -n "$current_head" ] && [ "$current_head" != "$EVENT_HEAD_SHA" ]; then |
There was a problem hiding this comment.
[Suggestion] R1-8: Still stands — the delay-job stale-head skip, a core gate of this PR, is pinned only by echo strings and the EVENT_HEAD_SHA env: the test asserts toContain('while this run queued') and toContain('should_review=false') — the latter is also emitted by the pre-existing draft-skip branch in the same step, so the asserted string proves nothing about which branch fired, and the guard's comparison line is pinned nowhere. Flipping != to = ships green — mutation ran this round: the suite stays at 192/192 — and would silently stop all delayed automatic reviews (every queued run whose head has NOT moved skips itself) while moved-head runs proceed against a stale head.
Suggested fix: replay the comparison under bash the way the salvage-percent parse is replayed — extract the guard block, drive it with current_head equal to and different from EVENT_HEAD_SHA (stub pr_data/gh), and assert should_review=true/should_review=false respectively; at minimum pin the condition line verbatim.
Fix witness: the positive/negative replay — flip the operator and exactly one of the two cases goes red.
中文说明
[Suggestion] R1-8:仍然存在——delay job 的过期 head 跳过是本 PR 的核心闸门,却只被 echo 字符串与 EVENT_HEAD_SHA env 钉住:测试断言 toContain('while this run queued') 与 toContain('should_review=false')——后者同样由同一步骤中既有的 draft 跳过分支输出,因此断言的字符串无法证明是哪个分支触发,守卫的比较行没有任何钉住。把 != 翻成 = 可以绿色通过——本轮已做变异实验:套件仍为 192/192——其后果是所有延迟自动评审被静默停止(head 未移动的排队 run 全部自我跳过),而 head 已移动的 run 反而去审过期 head。
建议修复:仿照 salvage-percent 解析的回放方式在 bash 中回放该比较——提取守卫块,分别以 current_head 等于/不等于 EVENT_HEAD_SHA 驱动,断言 should_review=true/should_review=false;至少逐字钉住条件行。
修复验证:正/负回放——翻转操作符后两个用例中恰好一个变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
There was a problem hiding this comment.
Deferred to the next round — this round's batch bound went to the five Criticals and their witnesses first. The finding still stands: the stale-head skip is pinned only by echo strings shared with the draft-skip branch, and the !=→= mutation surviving is a real hole. Planned fix is the suggested bash replay of the guard block with equal/different current_head, asserting should_review=true/false respectively.
推迟到下一轮——本轮批次上限优先安排了五条 Critical 及其验证。该发现仍然成立:过期 head 跳过只被与 draft 跳过分支共享的 echo 字符串钉住,!=→= 变异可存活是真实漏洞。计划按建议实现:在 bash 中回放守卫块,分别以相等/不等的 current_head 驱动,断言 should_review=true/should_review=false。
| ...extraEnv, | ||
| }, | ||
| }); | ||
| } catch (e) { |
There was a problem hiding this comment.
[Suggestion] R1-9: Still stands — the two salvage/supersede-cede early exits depend on exiting 0 (a non-zero cede exit would open the failure-fallback gate the cede exists to keep silent), but runScenario still swallows a non-zero exit (catch (e) { stdout = e.stdout ?? '' }, no status captured) and both cede replay tests assert only transcript content (r.raw substrings, r.attempts). The new trap-reap test asserts status === 0, but only for the extracted trap harness — the loop-level cede exits remain unverified. If a future edit turns either cede exit non-zero (or an errexit trip does), every existing assertion stays green while the job actually fails.
Suggested fix: capture and return the exit status in runScenario (e.g. status = e.status ?? 0 in the catch and status: 0 on the success path) and assert r.status === 0 in both cede scenarios.
Fix witness: the new assertions — mutate either cede's exit 0 to exit 1 and the cede scenarios go red.
中文说明
[Suggestion] R1-9:仍然存在——两处 salvage/supersede 让位的提前退出依赖以 0 退出(非零退出会打开让位本要保持沉默的失败兜底门),但 runScenario 仍吞掉非零退出(catch (e) { stdout = e.stdout ?? '' },未捕获状态码),两个让位回放测试也只断言输出内容(r.raw 子串、r.attempts)。新增的 trap 回收测试断言了 status === 0,但只针对提取的 trap harness——loop 层的让位退出仍未验证。若未来编辑把任一让位退出改成非零(或被 errexit 触发),所有现有断言保持绿色而 job 实际失败。
建议修复:在 runScenario 中捕获并返回退出状态(catch 中 status = e.status ?? 0,成功路径 status: 0),并在两个让位场景中断言 r.status === 0。
修复验证:新增断言本身——把任一让位的 exit 0 变异为 exit 1,让位场景必须变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
There was a problem hiding this comment.
Deferred to the next round — this round's batch bound went to the five Criticals and their witnesses first. The finding stands: runScenario still swallows non-zero exits, and the loop-level cede exits are only asserted by transcript content. Planned fix is the suggested one: capture the exit status in runScenario (status on both the success and catch paths) and assert r.status === 0 in both cede scenarios, red against an exit 0→exit 1 mutation.
推迟到下一轮——本轮批次上限优先安排了五条 Critical 及其验证。该发现成立:runScenario 仍吞掉非零退出,loop 层的让位退出只有输出内容断言。计划按建议实现:在 runScenario 中捕获退出状态(成功与 catch 两条路径),并在两个让位场景中断言 r.status === 0;对 exit 0→exit 1 变异必须变红。
The supersede/salvage cedes trusted agent-writable marker files by existence alone, letting a forged file suppress a whole review with the job green, and the composed-artifact latch trusted a bare existence check on an agent-derivable workspace path. Re-read the live head at every cede decision, latch only on a parseable composed verdict, delete the artifact in the per-attempt reset, and shape-check moved-to before the $GITHUB_OUTPUT write. Each guard is pinned by a replayed-bash witness that turns red when the guard is removed.
|
🤖 Addressed the latest review feedback (round 2/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 2/100 轮)。改动内容与我反驳保留之处如下: Autofix round summary — PR #10123 (review-hold-and-salvage)Commit: Thirteen findings arrived this round (5 Critical, 8 Suggestion). All 5 Criticals Addressed findings[Critical] R1-2 — per-attempt reset left the composed artifact behind (rc:3865698340)Reproduced: with a stale-but-valid composed artifact seeded, a moved head, and a [Critical] R2-1 — supersede-cede checks trusted
|
| Probe | Mutation | Witness test | Result |
|---|---|---|---|
| M1 | reset keeps the stale artifact | deletes a stale composed artifact | RED ✅ |
| M2a | latch back to bare -e |
latches only a real composed artifact | RED ✅ |
| M2b | latch block deleted | latches only a real composed artifact | RED ✅ |
| M3a | first supersede site unguarded | forged supersede file, unmoved head | RED ✅ |
| M3b | second supersede site unguarded | forged mid-attempt, unmoved head | RED ✅ |
| M4 | salvage-cede unguarded | forged salvage marker, unmoved head | RED ✅ |
| M5 | moved-to shape gate deleted | forged moved-to injection | RED ✅ |
Verification
npx vitest run --config ./scripts/tests/vitest.config.ts qwen-pr-review-workflow— 200/200 passed (192 pre-round; 8 new tests)npm run test:scripts— 1794 passed, 16 skipped, 1 failed:install-script.test.js > standalone release packaging > does not package audio-capture test artifacts— pre-existing and unrelated: it fails withENOENT ... packages/audio-capture/dist, and reproduces identically with this round's changes stashed (that package'sdistis not built in this environment); this round touches neither the packaging script nor that packagenpm run build— passed (exit 0)npm run typecheck— passed (exit 0)npm run lint— passed (exit 0)bash .github/scripts/check-workflow-size.sh— passed; baseline now exact (178626 bytes)- YAML parse of the modified workflow — passed
- Integration tests: not run — the change touches workflow YAML and scripts tests only, nothing exercised through the bundled CLI
npm run generate:settings-schema: not applicable — no settings source changed- Reproduction probes on pre-round HEAD for all five Criticals (outputs recorded) — each flipped after the fix
中文说明
Autofix 轮次总结 — PR #10123(review-hold-and-salvage)
提交:29d6fd4d28 — fix(ci): verify the live head before ceding and validate salvage signals
本轮收到 13 条发现(5 条 Critical、8 条 Suggestion)。所有 5 条 Critical 都在改动前先用探针对本轮前的 HEAD 做了复现,随后修复,并用"删除修复即变红"的 bash 回放验证测试钉住。与 Critical 修复直接配套的 4 条 Suggestion(R2-5、R2-6、R2-7、R2-9)一并纳入;其余 4 条 Suggestion 因单轮批次上限(Critical 优先)推迟到下一轮,并已在各自线程回复。评审级 CHANGES_REQUESTED(rv:5033921342,"部分审查——缺口已披露")本身没有超出内联发现的可执行内容;其披露的缺口(反向审计因时间预算中止、actionlint 内嵌 shell 限制)无需代码改动。
已处理的发现
[Critical] R1-2 — 按尝试重置遗漏了 compose 工件(rc:3865698340)
复现:放置陈旧但合法的 composed 工件、head 移动、尝试年龄 0 秒时,观察者重新锁存 compose-seen 并布防 salvage marker(marker=head-a、superseded=ABSENT)——0 秒的尝试得到 KEEP。修复:按尝试重置一并删除 $COMPOSED_ARTIFACT。验证:deletes a stale composed artifact in the per-attempt reset (replayed loop)——尝试 1 放置工件后以可重试方式失败,尝试 2 观察到工件已消失;从重置中删掉工件删除后该测试变红(探针 M1:expected 'present' to be 'absent')。链路的观察者侧(无工件 → 不锁存 → 低于阈值时 CEDE)由既有的无工件 CEDE 用例与下方锁存测试共同钉住。
[Critical] R2-1 — 两处 supersede 让位检查无条件信任 SUPERSEDE_FILE(rc:3865698363)
复现:head 未移动时放置 SUPERSEDE_FILE,loop 以 0 次尝试 exit 0("Superseded early"——无声的绿色压制,且不存在接替 run)。修复:共享的 live_head_moved 辅助函数在决策时刻重读 live head;仅当 live head 非空且不等于 $EXPECTED_HEAD_SHA 时才让位;读取失败或 head 未移动则落回正常尝试路径。验证:does not cede to a forged supersede file when the live head never moved(loop 前站点)与 ... forged mid-attempt ...(尝试后站点)——两者在本轮前代码上为红,且各自站点的守卫被移除后再次变红(探针 M3a/M3b)。三个既有的让位回放按发现要求改用真实 moved-head stub(STUB_LIVE_HEAD=head-b、EXPECTED_HEAD_SHA=head-a)以保持绿色。
[Critical] R2-2 — compose 锁存把裸 -e 存在性检查用于 agent 可写路径(rc:3865698373)
复现:30 秒尝试(深度 CEDE 区间)下,空的伪造工件触发锁存并布防 KEEP。修复:锁存现在要求工件可解析为 JSON 对象(composed verdict 即 JSON);写一半的残缺文件解析失败、下一次轮询再锁存。残余风险按 marker 逃生口的方式在代码中文档化:能写合法 JSON 的 agent 仍可锁存——但它本来就是这份评审的作者。验证:latches the compose signal only from a real composed artifact (replayed watcher)——空/{not json/null/[1,2] 工件均不得锁存(低于阈值时观察者仍让位);在本轮前代码上为红,移除校验或删除整个锁存块后再次变红(探针 M2a/M2b)。
[Critical] R2-3 — salvage 布防后的让位仅凭 marker 存在性(rc:3865698381)
复现:伪造 salvage-ok marker + 失败尝试 → 1 次尝试后 exit 0(无声压制;无 marker 的对照臂正确地两次尝试后 FAIL)。修复:salvage 让位条件加入同样的 live_head_moved 闸门;head 未移动的伪造 marker 落回正常重试/失败路径。验证:retries and fails when the salvage marker is forged and the head never moved (replayed loop)——本轮前为红,移除闸门后再次变红(探针 M4)。既有的 salvage 让位回放改用其所声称钉住的 moved-head stub。
[Critical] R2-4 — salvage_moved_to 把 agent 可写的原始内容注入 $GITHUB_OUTPUT(rc:3865698386)
复现:多行 moved-to 在步骤自身写入之后多出 docs_only_medium=true 与 completion_line=forged 两条 $GITHUB_OUTPUT 行。修复:写输出前,moved-to 必须恰好是 40 位小写十六进制(观察者唯一会写的形状);其他任何形状——包括能在命令替换中存活的内嵌换行——一律降级为 unknown。验证:degrades a forged moved-to instead of injecting outputs (replayed block) 断言 $GITHUB_OUTPUT 恰好新增 salvaged=true/salvage_moved_to=unknown;本轮前为红,删除形状闸门后再次变红(探针 M5)。
[Suggestion] R2-5 — 两处 supersede 让位块是逐字副本(rc:3865698396)
在两处都实现 R2-1 会复制新闸门,因此消息 + 退出收敛为一个 cede_superseded 函数(定义在 retry-loop 提取窗口内,逐字回放仍可见),两处调用。新增结构性钉住:一份定义、共三处出现,仿照套件中 halve-budget-floor 的先例。
[Suggestion] R2-6 — compose 锁存从未被端到端覆盖(rc:3865698403)
R2-2 验证的合法工件臂正是所要求的用例:runWatcher 在 composeSeen: false、低于阈值时放置真实 composed verdict,断言 marker === 'head-a' 且无 supersede 文件——端到端覆盖 工件→锁存→KEEP。删除锁存块后变红(探针 M2b)。
[Suggestion] R2-7 — loop 后的 marker→outputs 块从未被执行(rc:3865698427)
该块现在被提取并在 bash 中回放:marker 存在 + 40 位十六进制 moved-to → 写出两个输出;marker 缺失 → 两者均不出现(翻转条件会让每个普通 run 都发布历史 head 说明);无目的地文件 → salvage_moved_to=unknown。说明步骤的 MOVED_TO env 现被钉到 steps.review.outputs.salvage_moved_to。
[Suggestion] R2-9 — 基线条目与提交文件实际大小不符(rc:3865698450)
确认:文件 176231 字节而基线记录 176219。本轮编辑再次改变了大小,因此 .size-baseline 现记录编辑后的精确大小(178626,wc -c 验证),bash .github/scripts/check-workflow-size.sh 绿色。
推迟到下一轮(已在线程回复)
- R2-8(rc:3865698439)——观察者
|| continue容错测试:有效;因批次上限推迟。 - R2-10(rc:3865698458)——salvage 逃生口仅在
AUTO_REVIEW下布防:有效;因批次上限推迟。给实施者的提示:loop 后的输出读取使用不带:-默认值的$QWEN_CI_REVIEW_SALVAGE_OK_FILE,当导出变为条件性时必须补上(步骤运行于set -u)。 - R1-8(rc:3865698467)——delay job 过期 head 跳过的回放:有效;因批次上限推迟。
- R1-9(rc:3865698475)——
runScenario捕获退出状态:有效;因批次上限推迟。
变异探针(每个新守卫都有自己的红色验证)
| 探针 | 变异 | 验证测试 | 结果 |
|---|---|---|---|
| M1 | 重置保留陈旧工件 | deletes a stale composed artifact | 红 ✅ |
| M2a | 锁存退回裸 -e |
latches only a real composed artifact | 红 ✅ |
| M2b | 删除整个锁存块 | latches only a real composed artifact | 红 ✅ |
| M3a | 第一处 supersede 站点去守卫 | forged supersede file, unmoved head | 红 ✅ |
| M3b | 第二处 supersede 站点去守卫 | forged mid-attempt, unmoved head | 红 ✅ |
| M4 | salvage 让位去守卫 | forged salvage marker, unmoved head | 红 ✅ |
| M5 | 删除 moved-to 形状闸门 | forged moved-to injection | 红 ✅ |
验证
npx vitest run --config ./scripts/tests/vitest.config.ts qwen-pr-review-workflow— 200/200 通过(本轮前 192;新增 8 个测试)npm run test:scripts— 1794 通过、16 跳过、1 失败:install-script.test.js > standalone release packaging > does not package audio-capture test artifacts——既有且无关:失败原因是ENOENT ... packages/audio-capture/dist,在本轮改动被 stash 的情况下同样复现(该包的dist未在此环境构建);本轮未触碰打包脚本或该包npm run build— 通过(exit 0)npm run typecheck— 通过(exit 0)npm run lint— 通过(exit 0)bash .github/scripts/check-workflow-size.sh— 通过;基线现为精确值(178626 字节)- 修改后的 workflow YAML 解析 — 通过
- 集成测试:未运行——改动仅涉及 workflow YAML 与 scripts 测试,不经过捆绑 CLI 执行
npm run generate:settings-schema:不适用——未改动任何 settings 源- 五条 Critical 均在本轮前 HEAD 上完成复现探针(输出已记录),修复后全部翻转
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。
Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。
🧠 Handled by Qwen Code · model/模型 qwen3.8-max
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: reverse audit — stopped before round 7 by the review time budget.
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
Convergence: round 3 posted 14 inline comment(s), 9 of them reported for the first time; the previous round posted 13 (11 new). Findings keep coming back to the same files: .github/workflows/qwen-code-pr-review.yml (findings in rounds 1, 2; 6 more now); scripts/tests/qwen-pr-review-workflow.test.js (findings in round 1; 3 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)
中文说明
仅完成部分审查,审查缺口已披露。
未审查:反向审计——评审时间预算不足,未能开始第 7 轮。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
收敛情况:第 3 轮发布了 14 条行内评论,其中 9 条是首次提出;上一轮发布了 13 条(其中 11 条首次提出)。发现反复回到同一批文件:.github/workflows/qwen-code-pr-review.yml(第 1、2 轮已出过发现,本轮又有 6 条);scripts/tests/qwen-pr-review-workflow.test.js(第 1 轮已出过发现,本轮又有 3 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)
— qwen3.8-max via Qwen Code /review (v0.22.2)
| if [ ! -f "${SALVAGE_DIR}/compose-seen" ] && [ -e "$COMPOSED_ARTIFACT" ] \ | ||
| && node -e 'const v=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));if(typeof v!=="object"||v===null||Array.isArray(v))process.exit(1);' "$COMPOSED_ARTIFACT" 2>/dev/null; then |
There was a problem hiding this comment.
[Critical] R2-2: (fix-induced) The JSON-parse validation that closed R2-2 introduced a blocking read of the same agent-derivable path: the latch tests [ -e ] instead of [ -f ], so a FIFO (or a symlink to a device) planted at the artifact path blocks the node -e readFileSync forever and permanently kills the supersede watcher. The path is fully derivable by the reviewed agent (GITHUB_WORKSPACE + PR_NUMBER are both in its environment, and it runs yolo over attacker-controlled PR content — npm ci alone runs PR lifecycle scripts); the per-attempt rm -f reset already ran before the attempt, so it does not remove the plant. With the watcher dead, no CEDE and no KEEP ever fires for the rest of the run: every later push burns the superseded run's entire remaining budget while the queued replacement waits, and at posting time the un-armed marker sends guard_pr_write to exit 90, discarding the review — the #9729/#10110 waste class re-introduced through the hardened latch.
Witness (probe): [ -e fifo ] TRUE, [ -f fifo ] FALSE; the workflow's exact node -e line against a FIFO → exit 124 under timeout (blocked indefinitely in open()); with an implied [ -f ] gate the probe flips — artifact-fifo.json: not latched while artifact.json: LATCHED.
| if [ ! -f "${SALVAGE_DIR}/compose-seen" ] && [ -e "$COMPOSED_ARTIFACT" ] \ | |
| && node -e 'const v=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));if(typeof v!=="object"||v===null||Array.isArray(v))process.exit(1);' "$COMPOSED_ARTIFACT" 2>/dev/null; then | |
| if [ ! -f "${SALVAGE_DIR}/compose-seen" ] && [ -f "$COMPOSED_ARTIFACT" ] \ | |
| && node -e 'const v=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));if(typeof v!=="object"||v===null||Array.isArray(v))process.exit(1);' "$COMPOSED_ARTIFACT" 2>/dev/null; then |
Optionally also bound the check with timeout 30 node -e ... so no regular-file payload can wedge a poll. Fix witness: extend latches the compose signal only from a real composed artifact (replayed watcher) to plant a FIFO at the artifact path (bounded execFileSync timeout) and assert the watcher still cedes below threshold — removing the [ -f ] gate makes it hang/red.
中文说明
[Critical] R2-2:(由修复引入)关闭 R2-2 的 JSON 解析校验在同一条 agent 可推导路径上引入了阻塞读取:锁存使用 [ -e ] 而非 [ -f ],因此在工件路径放置 FIFO(或指向设备的符号链接)会让 node -e 的 readFileSync 永久阻塞,彻底杀死 supersede 观察者。该路径可被评审 agent 完全推导(GITHUB_WORKSPACE 与 PR_NUMBER 都在其环境中,且它以 yolo 模式执行攻击者可控的 PR 代码——仅 npm ci 就会执行 PR 的生命周期脚本);按尝试的 rm -f 重置在尝试开始前已执行,不会清除该放置。观察者死亡后,本次运行余下时间内既不会 CEDE 也不会 KEEP:之后每次 push 都会让被取代的 run 烧掉全部剩余预算,排队的接替 run 只能等待,发布时未布防的 marker 又让 guard_pr_write 走到 exit 90,整份评审被丢弃——#9729/#10110 要消除的浪费经由加固后的锁存重新出现。
验证证据(探针):[ -e fifo ] 为真、[ -f fifo ] 为假;workflow 中的 node -e 原行作用于 FIFO → 超时被杀(exit 124,阻塞在 open());改用 [ -f ] 闸门后翻转——FIFO 不锁存,真实工件正常锁存。
修复:改用 [ -f ](拒绝 FIFO、设备文件及指向它们的符号链接),可选再套 timeout 30。修复验证:在 latches the compose signal only from a real composed artifact (replayed watcher) 中放置 FIFO(限制执行超时),断言低于阈值时观察者仍让位;移除 [ -f ] 闸门后该测试必须变红/挂起。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| echo "qwen-review-salvage: head moved to ${live_head} ${elapsed}s in (compose_seen=${compose_seen}) — finishing and posting against reviewed head ${EXPECTED_HEAD_SHA} (#10110)" | ||
| return 0 | ||
| fi | ||
| printf '%s' "$live_head" > "$SUPERSEDE_FILE" |
There was a problem hiding this comment.
[Critical] R3-3: Every signal-file write-open performed by the watcher and the per-attempt reset is an unguarded blocking open of an agent-derivable path — the write-side mirror of R2-2/R3-2. SALVAGE_DIR is the dirname of the exported QWEN_CI_REVIEW_SALVAGE_OK_FILE, so the reviewed agent (yolo, same runner/user) can mkfifo the fixed workflow-public names inside it, and opening a FIFO O_WRONLY blocks until a reader appears. Unguarded sites: : > compose-seen (~1624 — a FIFO defeats even the R2-2 fix, since ! -f is true for a FIFO and : > then blocks), printf > moved-to and printf > salvage-ok (~1649-1650), printf > SUPERSEDE_FILE (here, ~1654), and date > attempt-start (~1586, ~1702); the watcher's cat of attempt-start (~1638) also lacks an -f gate. Concrete sequence: the agent plants mkfifo $SALVAGE_DIR/superseded early; a real head move arrives below threshold; the watcher decides CEDE and blocks forever on this redirect — pkill never runs, SUPERSEDE_FILE never appears, the step's [ -f "$SUPERSEDE_FILE" ] checks stay false, and the run burns the entire remaining budget before guard_pr_write exits 90 while the queued replacement waits the whole time. Plants at moved-to/salvage-ok turn a KEEP into "marker never armed → exit 90 at post"; a plant at attempt-start blocks the watcher's per-poll cat or hangs the main step itself.
Witness (6-arm probe on the verbatim watcher): FIFO at superseded → exit 124, pkills ABSENT, no supersede file; FIFO at moved-to (KEEP path) → exit 124, marker NEVER ARMED; FIFO at compose-seen + valid artifact → exit 124 despite the ! -f gate; FIFO at attempt-start → the ungated cat hangs; the mktemp + mv -f fix arm flips to a clean CEDE (superseded=head-b, pkills logged, exit 0). POSIX check: printf x > fifo exit 124; mv -f over a FIFO exit 0 (rename never opens the target).
Fix — never write-open a derivable path; write aside and rename at all five write sites:
tmp="$(mktemp "${SALVAGE_DIR}/.sig.XXXXXX")" && printf '%s' "$content" > "$tmp" && mv -f "$tmp" "$target" || rm -f "$tmp"and gate the watcher's attempt-start read with [ -f "${SALVAGE_DIR}/attempt-start" ] before the cat (the existing || true fallback to START_TS already tolerates the miss). Fix witness: a runWatcher replay pre-planting a FIFO at ${dir}/superseded (and variants at moved-to/attempt-start), run below threshold, asserting within a bounded timeout that the CEDE outputs still land (superseded === 'head-b', pkilled true); with the direct > writes in place the harness hangs and the test goes red on timeout.
中文说明
[Critical] R3-3:观察者与按尝试重置执行的每一次信号文件写打开,都是对 agent 可推导路径的无防护阻塞打开——R2-2/R3-2 的写侧镜像。SALVAGE_DIR 即导出的 QWEN_CI_REVIEW_SALVAGE_OK_FILE 的 dirname,被评审 agent(yolo、同 runner 同用户)可以在其中对这些固定的工作区公开文件名执行 mkfifo,而以 O_WRONLY 打开 FIFO 会阻塞到出现读者为止。无防护站点:: > compose-seen(~1624——FIFO 甚至能击穿 R2-2 的修复,因为 ! -f 对 FIFO 为真,随后 : > 阻塞)、printf > moved-to 与 printf > salvage-ok(~1649-1650)、printf > SUPERSEDE_FILE(此处,~1654)、date > attempt-start(~1586、~1702);观察者对 attempt-start 的 cat(~1638)同样缺少 -f 闸门。具体过程:agent 预先放置 mkfifo $SALVAGE_DIR/superseded;低于阈值时真实 head 移动到来;观察者决定 CEDE 并在该重定向上永久阻塞——pkill 从未执行,SUPERSEDE_FILE 永不出现,步骤中的 [ -f "$SUPERSEDE_FILE" ] 检查恒为假,run 烧掉整个剩余预算后由 guard_pr_write 以 exit 90 收场,排队的接替 run 全程等待。放置在 moved-to/salvage-ok 会把 KEEP 变成「marker 从未布防 → 发布时 exit 90」;放置在 attempt-start 会阻塞观察者的逐次轮询 cat,或直接挂起主步骤本身。
验证证据(对逐字提取的 watcher 做 6 臂探针):superseded 处 FIFO → exit 124、pkill 缺失、无 supersede 文件;moved-to 处 FIFO(KEEP 路径)→ exit 124、marker 从未布防;compose-seen 处 FIFO + 合法工件 → 尽管有 ! -f 闸门仍 exit 124;attempt-start 处 FIFO → 无闸门 cat 挂起;mktemp + mv -f 修复臂翻转为干净 CEDE(superseded=head-b、pkill 已记录、exit 0)。POSIX 验证:printf x > fifo exit 124;mv -f 覆盖 FIFO exit 0(重命名不打开目标)。
修复:五个写站点一律改为「旁路写入 + 重命名」,永不写打开可推导路径;并在观察者读取 attempt-start 前加 [ -f ] 闸门。修复验证:runWatcher 回放预先放置 FIFO,低于阈值运行,限制超时内断言 CEDE 输出仍然落地;保留直接 > 写入时回放挂起、测试变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| # goes with the latch: a retryable death after compose skips the | ||
| # skill's Step 9 cleanup, and a surviving artifact would re-latch | ||
| # the watcher within one poll. | ||
| rm -f "${SALVAGE_DIR}/compose-seen" "${COMPOSED_ARTIFACT:-}" |
There was a problem hiding this comment.
[Critical] R1-2: (fix-induced) The per-attempt reset that closed R1-2 — now correctly deleting both the compose-seen latch and the composed artifact — runs only at the top of the loop iteration, i.e. AFTER the 60-second retry backoff, so the watcher's KEEP/CEDE decision during the backoff reads the dead attempt's state, violating this hunk's own per-attempt invariant. Trigger: attempt 1 reaches compose (the latch fires, or the artifact survives because a retryable death skips the skill's Step 9 cleanup — the exact case this comment describes), then dies retryably (the canonical [API Error: 503 upstream overloaded]); a push lands during the deterministic 60s backoff. The watcher poll sees compose_seen=true (re-latched from the surviving artifact within one poll) or attempt 1's elapsed fraction already past SALVAGE_ELAPSED_PERCENT, and arms the salvage marker instead of writing SUPERSEDE_FILE. Attempt 2 then re-reviews the dead head from zero for up to the entire remaining budget while the replacement run waits PENDING — the exact #10110 waste class, surviving in the retry-backoff corner; the top-of-loop re-check cannot catch it because no SUPERSEDE_FILE was written.
Witness (probe over the verbatim-extracted watcher + loop, head flips as the backoff starts): PR code — qwen-review-salvage: head moved to head-b 3s in (compose_seen=true) — finishing and posting against reviewed head head-a, MARKER_ARMED=head-a, SUPERSEDE_FILE=no, attempt 2 started and re-reviewed the dead head; with the same reset also run before the backoff — head moved to head-b 0s in, before the salvage threshold, SUPERSEDE_FILE=head-b, pkilled=yes, MARKER_ARMED=no, Superseded early: … ceding to the replacement run, only attempt 1 ran.
Fix: run the reset before the backoff as well — immediately before sleep "$RETRY_BACKOFF_SECONDS" add:
rm -f "${SALVAGE_DIR}/compose-seen" "${COMPOSED_ARTIFACT:-}"
date +%s > "${SALVAGE_DIR}/attempt-start"keeping the top-of-loop copy for attempt 1. Fix witness: a new replayed-loop scenario (same family as supersede_during_backoff): attempt 1 reaches compose then dies retryable; the stubbed backoff sleep asserts compose-seen is absent and writes $SUPERSEDE_FILE; expect attempts == 1 and Superseded early: — removing the pre-backoff reset makes it red.
中文说明
[Critical] R1-2:(由修复引入)关闭 R1-2 的按尝试重置——现在已正确删除 compose-seen 锁存与 composed 工件——只在循环迭代顶部执行,即 60 秒重试退避之后,因此退避期间观察者的 KEEP/CEDE 决策仍会读到已死尝试的状态,违反本代码块自己声明的按尝试不变量。触发过程:尝试 1 到达 compose(锁存已置位,或工件因可重试死亡跳过 skill 的 Step 9 清理而残留——正是此处注释描述的情形),随后以可重试方式失败(典型的 [API Error: 503 upstream overloaded]);push 落在这段确定性的 60 秒退避内。观察者轮询看到 compose_seen=true(一次轮询内从残留工件重新锁存)或尝试 1 已过 SALVAGE_ELAPSED_PERCENT 的耗时比例,于是布防 salvage marker 而非写出 SUPERSEDE_FILE。尝试 2 从零开始重审过期 head,最长可烧掉整个剩余预算,而接替 run 只能排队等待——正是 #10110 要消除的浪费,在重试退避角落存活;循环顶部的复查无法捕获,因为 SUPERSEDE_FILE 从未被写出。
验证证据(对逐字提取的观察者 + 循环做探针,head 在退避开始时翻转):现状代码——观察者在退避期间裁决,MARKER_ARMED=head-a、SUPERSEDE_FILE=no,尝试 2 启动并重审死 head;在退避前同样执行重置后——SUPERSEDE_FILE=head-b、pkilled=yes、MARKER_ARMED=no,输出 Superseded early: … ceding,仅执行尝试 1。
修复:在 sleep "$RETRY_BACKOFF_SECONDS" 之前同样执行该重置(保留循环顶部的副本用于尝试 1)。修复验证:新增回放场景——尝试 1 到达 compose 后可重试死亡,退避 stub 断言 compose-seen 已删除并写出 $SUPERSEDE_FILE,断言仅一次尝试且输出 Superseded early:;移除退避前置重置后必须变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| # lines in $GITHUB_OUTPUT, flipping downstream gates like | ||
| # docs_only_medium. The watcher only ever writes a 40-hex head | ||
| # SHA here, so any other shape degrades to `unknown`. | ||
| salvage_moved_to="$(cat "${SALVAGE_DIR}/moved-to" 2>/dev/null || echo unknown)" |
There was a problem hiding this comment.
[Critical] R3-2: moved-to is attacker-writable (SALVAGE_DIR derives from the exported QWEN_CI_REVIEW_SALVAGE_OK_FILE) and is read here with a blocking cat without the [ -f ] regular-file gate its siblings got this round; a FIFO planted there hangs a finished, successful review step into a job-timeout cancel. The marker itself needs only to be a regular file (the gate at ~1752 checks nothing else), so the agent can forge the marker and mkfifo moved-to. After a successful attempt the post-loop block enters on the armed marker and cat blocks forever (no writer ever opens the FIFO); the step never reaches the trap or the salvaged/salvage_moved_to outputs; QWEN_REVIEW_JOB_TIMEOUT_MINUTES cancels review-pr alone — a review that completed and posted shows as cancelled, the historical-head note step never runs, and the fallback gate explicitly admits needs.review-pr.result == 'cancelled' when the upstream jobs completed, posting a false failure comment on a PR whose review already posted. The same class applies, more weakly, to cat "${QWEN_CI_REVIEW_SALVAGE_OK_FILE}" in guard_pr_write (no -f gate before the read), which can hang the posting path when the head moved.
Witness (probe): timeout 5 around the exact salvage_moved_to="$(cat ./fifo 2>/dev/null || echo unknown)" shape → cat exit 124 (blocked indefinitely); at HEAD the gate is [ -f "$QWEN_CI_REVIEW_SALVAGE_OK_FILE" ] (marker only) with no -f before the moved-to cat.
| salvage_moved_to="$(cat "${SALVAGE_DIR}/moved-to" 2>/dev/null || echo unknown)" | |
| salvage_moved_to="unknown" | |
| if [ -f "${SALVAGE_DIR}/moved-to" ]; then | |
| salvage_moved_to="$(cat "${SALVAGE_DIR}/moved-to" 2>/dev/null || echo unknown)" | |
| fi |
and add the same -f check before the cat in guard_pr_write's salvage escape. Fix witness: extend writes the salvage outputs only for an armed marker (replayed block) to a FIFO moved-to case asserting salvage_moved_to=unknown with a bounded execFileSync timeout; removing the guard makes it hang/red.
中文说明
[Critical] R3-2:moved-to 可被攻击者写入(SALVAGE_DIR 由导出的 QWEN_CI_REVIEW_SALVAGE_OK_FILE 推导),此处用阻塞的 cat 读取,却没有本轮其他同类读取获得的 [ -f ] 常规文件闸门;在此放置 FIFO 会把一个已成功完成的评审步骤挂成 job 超时取消。marker 本身只需是常规文件(~1752 的闸门只检查这一点),因此 agent 可以伪造 marker 并对 moved-to 执行 mkfifo。成功尝试结束后,loop 后块因 marker 已布防而进入,cat 永久阻塞(永远不会有写者打开该 FIFO);步骤到不了 trap,也写不出 salvaged/salvage_moved_to 输出;QWEN_REVIEW_JOB_TIMEOUT_MINUTES 仅取消 review-pr——一份已完成并已发布的评审显示为被取消,历史 head 说明步骤不再执行,而兜底门在上游 job 完成时明确接受 needs.review-pr.result == 'cancelled',会在评审已经发布的 PR 上发布虚假的失败评论。同类问题(较弱)也存在于 guard_pr_write 中的 cat "${QWEN_CI_REVIEW_SALVAGE_OK_FILE}"(读取前无 -f 闸门),head 移动时可能挂起发布路径。
验证证据(探针):对原样的 salvage_moved_to="$(cat ./fifo 2>/dev/null || echo unknown)" 形状套 timeout 5 → cat exit 124(无限阻塞);HEAD 处闸门仅为 [ -f "$QWEN_CI_REVIEW_SALVAGE_OK_FILE" ](只检查 marker),moved-to 的 cat 前无 -f。
修复:仅在 [ -f ] 时读取 moved-to,否则降级为 unknown;guard_pr_write 的 salvage 逃生口中的 cat 前同样加 -f。修复验证:在 writes the salvage outputs only for an armed marker (replayed block) 中新增 FIFO moved-to 用例,限制超时并断言 salvage_moved_to=unknown;移除闸门后挂起/变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| PRM: promptFile, | ||
| ...extraEnv, |
There was a problem hiding this comment.
[Critical] R3-1: The replay harness spreads process.env into the spawned bash, and this delta moved the AUTO_REVIEW-gated background watcher arming (supersede_watcher &) inside the extraction window the harness executes — so any environment exporting AUTO_REVIEW=true arms the watcher inside every runScenario replay. That includes this workflow's own review-pr step env (lines 641/674/894 set AUTO_REVIEW for exactly the dogfooding lane that verifies changes to this file): the harness has no REPO, so under set -u the watcher's gh pr view "$PR_NUMBER" --repo "$REPO" fails unbound, || continue keeps the loop alive against the instant-exit sleep stub, and the orphaned watcher holds the stdout/stderr pipes execFileSync waits on — vitest's 30s testTimeout cannot interrupt a synchronous child, so the suite hangs rather than fails.
Witness (executed in this review job, which exports AUTO_REVIEW=true): the suite hangs >600s per run across 4 runs, zero of 200 tests completing, orphaned bashes flooding environment: line 226: REPO: unbound variable; the same command with env -u AUTO_REVIEW → 200/200 passed in 4.34s; the merge base has no background arming in the window (hang introduced by this delta). Official CI lanes stay green only because they never export AUTO_REVIEW.
| PRM: promptFile, | |
| ...extraEnv, | |
| PRM: promptFile, | |
| AUTO_REVIEW: 'false', | |
| ...extraEnv, |
(pin after the spread is the minimal variant; an explicit env allowlist is the more thorough one — and correct the now-false stub comment claiming the retry backoff is the ONLY sleep in the extracted loop: the watcher's poll loop is now in it too). Do not change the workflow's production arming. Fix witness: a replay injecting AUTO_REVIEW: 'true' via extraEnv must complete with OK outcome=success; removing the pin turns it into a hang.
中文说明
[Critical] R3-1:回放 harness 把 process.env 原样展开进生成的 bash,而本 delta 把 AUTO_REVIEW 门控的后台观察者布防(supersede_watcher &)移进了 harness 执行的提取窗口——因此任何导出 AUTO_REVIEW=true 的环境都会在每次 runScenario 回放中布防观察者。这恰恰包括本工作流自己的 review-pr 步骤环境(641/674/894 行正是为验证本文件改动的 dogfood 通道设置 AUTO_REVIEW):harness 未定义 REPO,在 set -u 下观察者的 gh pr view "$PR_NUMBER" --repo "$REPO" 以未绑定变量失败,|| continue 让循环对着立即退出的 sleep stub 空转,孤儿观察者持有 execFileSync 等待的 stdout/stderr 管道——vitest 的 30 秒 testTimeout 无法中断同步子进程,套件表现为挂起而非失败。
验证证据(在导出 AUTO_REVIEW=true 的本评审 job 中执行):套件每次运行挂起超过 600 秒,共 4 次,200 个测试无一完成,孤儿 bash 持续刷出 environment: line 226: REPO: unbound variable;同一命令加 env -u AUTO_REVIEW → 200/200 通过,耗时 4.34 秒;合并基线的提取窗口内无后台布防(挂起由本 delta 引入)。官方 CI 通道保持绿色仅因其从不导出 AUTO_REVIEW。
修复:在展开后钉住 AUTO_REVIEW: 'false'(最小改法;更彻底的是显式环境变量白名单),并更正现已失实的「重试退避是提取窗口中唯一的 sleep」stub 注释。不要改动工作流的生产布防。修复验证:通过 extraEnv 注入 AUTO_REVIEW: 'true' 的回放必须正常完成;移除该钉住则挂起。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| wait "$WATCHER_PID" 2>/dev/null || true | ||
| WATCHER_PID="" | ||
| fi | ||
| if [ -f "$QWEN_CI_REVIEW_SALVAGE_OK_FILE" ]; then |
There was a problem hiding this comment.
[Suggestion] R2-3: Still stands (suppression arm fixed; residual arm re-reported) — the cede-suppression arm of R2-3 was closed by the live_head_moved gate, but its second arm remains: this outputs block trusts bare marker existence, the one marker consumer that does not follow the diff's own live-head-re-check policy stated at the cede sites' comment. Both the marker path (QWEN_CI_REVIEW_SALVAGE_OK_FILE) and its expected content (QWEN_CI_REVIEW_EXPECTED_HEAD_SHA) are exported into the reviewed agent's environment, so during an ordinary automatic review where the head NEVER moves the agent writes the expected-head value into the marker path: the guard_pr_write escape is a no-op (current head equals expected), the review completes and posts normally — but this block then emits salvaged=true and salvage_moved_to=unknown, and the Report salvaged historical-head review step's if: chain passes, posting "head moved to unknown while this review was in flight" on an unmoved PR and seeding a <!-- qwen-review-salvaged --> anchor the note says the autofix loop consumes. The escape's residual-risk comment argues only "forging it only lets it post to an old head" — a path that posts no review but a false CI-identity comment is a separate outcome it never argues.
Witness (4-arm probe over the verbatim block): forged marker + unmoved head → GITHUB_OUTPUT=[salvaged=true|salvage_moved_to=unknown|]; no-marker control → no outputs; fix arm (gate on live_head_moved, stub gh reporting the unmoved head) → no outputs; fix + real move → outputs flow normally.
| if [ -f "$QWEN_CI_REVIEW_SALVAGE_OK_FILE" ]; then | |
| if [ -f "$QWEN_CI_REVIEW_SALVAGE_OK_FILE" ] && live_head_moved; then |
(live_head_moved is already in scope; its failed-gh fall-through adds no false-negative risk beyond what the cede paths already accept.) Also extend the residual-risk comment to cover this consumer. Fix witness: a runSalvageOutputs case — marker present + gh stub returning the expected head (unmoved) must emit no outputs; removing the live-head gate makes it red (today's runSalvageOutputs({}) pin encodes the undefended behavior).
中文说明
[Suggestion] R2-3:仍然存在(压制臂已修复;残余臂重新上报)——R2-3 的让位压制臂已由 live_head_moved 闸门关闭,但其第二臂仍在:该输出块仅凭 marker 存在性信任,是唯一未遵循 diff 自己在让位站点注释中声明的「决策时重读 live head」策略的 marker 消费者。marker 路径(QWEN_CI_REVIEW_SALVAGE_OK_FILE)与其期望内容(QWEN_CI_REVIEW_EXPECTED_HEAD_SHA)都导出到被评审 agent 的环境中,因此在 head 从未移动的普通自动评审中,agent 可以把期望 head 值写入 marker 路径:guard_pr_write 逃生口此时是空操作(当前 head 等于期望值),评审正常完成并发布——但该块随后写出 salvaged=true 与 salvage_moved_to=unknown,Report salvaged historical-head review 步骤的 if: 链通过,在 head 未移动的 PR 上发布「head moved to unknown while this review was in flight」,并种下说明中声称供 autofix 循环消费的 <!-- qwen-review-salvaged --> 锚点。逃生口的残余风险注释只论证了「伪造仅允许向旧 head 发布」——一条不发布评审、只发布虚假 CI 身份评论的路径是它从未论证的另一结果。
验证证据(对逐字提取的块做 4 臂探针):伪造 marker + head 未移动 → GITHUB_OUTPUT=[salvaged=true|salvage_moved_to=unknown|];无 marker 对照 → 无输出;修复臂(以 live_head_moved 为闸门,stub gh 报告未移动)→ 无输出;修复 + 真实移动 → 输出正常。
修复:输出块与让位出口同样以 live_head_moved 为闸门(其 gh 读取失败时的落回不会带来超出既有让位路径的误报风险),并把残余风险注释扩展到该消费者。修复验证:runSalvageOutputs 用例——marker 存在 + gh stub 返回期望 head(未移动)必须无输出;移除 live-head 闸门后变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| // The cede exits re-read the live head before trusting their marker | ||
| // files; scripted per test (empty output = failed read / unmoved head, | ||
| // which must NOT cede). | ||
| write('gh', '#!/bin/bash\necho "${STUB_LIVE_HEAD:-}"\n'); |
There was a problem hiding this comment.
[Suggestion] R3-4: The [ -n "$live" ] conjunct this fix round added to live_head_moved — the exact defense against a failed gh read at cede time — is untested. The stub comment above this line declares "empty output = failed read / unmoved head, which must NOT cede", but no scenario reaches live_head_moved with an empty STUB_LIVE_HEAD: every cede-reaching test sets it to head-a/head-b, and the one marker-armed test without it short-circuits before the call. Mutation: delete [ -n "$live" ] && — every existing test stays green ("" != "head-a" is true anyway). Concrete trigger: a forged or stale SUPERSEDE_FILE (the exact threat the live-head re-check was added for) plus a transient gh failure at decision time → the mutant cedes (exit 0, no review posted, no replacement run) — the silent green suppression the forgery tests exist to prevent; the shipped code correctly returns rc=1.
Witness (mutant run): baseline Tests 200 passed (200); mutant (guard removed) Tests 200 passed (200) — survives. Divergence demo on the extracted function: forged SUPERSEDE_FILE + empty gh output → shipped code rc=1 (does NOT cede) vs mutant rc=0 (CEDES: silent suppression).
Fix: add one replayed-loop test — pre-create SUPERSEDE_FILE, run scenario transient_persist, leave STUB_LIVE_HEAD unset; expect 2 attempts and FAIL (no Superseded early:). Aim the same input shape at QWEN_CI_REVIEW_SALVAGE_OK_FILE to cover the marker path. Fix witness: that new test; removing [ -n "$live" ] && from live_head_moved in the workflow makes it fail (observes Superseded early: and 1 attempt instead of 2/FAIL).
中文说明
[Suggestion] R3-4:本轮修复为 live_head_moved 新增的 [ -n "$live" ] 合取项——正是防御让位时刻 gh 读取失败的关键——没有任何测试覆盖。此线上方的 stub 注释声明「空输出 = 读取失败 / head 未移动,绝不能让位」,但没有场景在 STUB_LIVE_HEAD 为空时到达 live_head_moved:所有到达让位检查的测试都设置为 head-a/head-b,唯一未设置的带 marker 测试在调用前就短路退出。变异:删除 [ -n "$live" ] && ——所有现有测试保持绿色("" != "head-a" 本来就为真)。具体触发:伪造或过期的 SUPERSEDE_FILE(正是 live-head 复查要防的威胁)加上决策时刻的瞬时 gh 失败 → 变异体会让位(exit 0,不发布评审,也没有接替 run)——伪造测试本要防止的无声绿色压制;现状代码正确返回 rc=1。
验证证据(变异运行):基线 Tests 200 passed (200);变异体(移除守卫)Tests 200 passed (200)——存活。对提取函数的分歧演示:伪造 SUPERSEDE_FILE + 空 gh 输出 → 现状代码 rc=1(不让位)对比变异体 rc=0(让位:无声压制)。
修复:新增一个回放测试——预建 SUPERSEDE_FILE、运行 transient_persist 场景、不设置 STUB_LIVE_HEAD;断言 2 次尝试且输出 FAIL (无 Superseded early:)。同样的输入形状也指向 QWEN_CI_REVIEW_SALVAGE_OK_FILE 以覆盖 marker 路径。修复验证:该新测试本身;从工作流删除 [ -n "$live" ] && 后必须失败。
— qwen3.8-max via Qwen Code /review (v0.22.2)
There was a problem hiding this comment.
Deferred to the next round under the ~8-finding bound (Criticals first). The [ -n "$live" ] conjunct is the exact defense against a failed gh read at cede time; the replayed-loop witness (pre-created SUPERSEDE_FILE, transient_persist, STUB_LIVE_HEAD unset, expecting 2 attempts and FAIL) lands with the next batch.
中文说明
受约 8 条的轮次上限推迟到下一轮(Critical 优先)。[ -n "$live" ] 合取项正是防御让位时刻 gh 读取失败的关键;回放式见证(预建 SUPERSEDE_FILE、transient_persist、不设置 STUB_LIVE_HEAD,断言 2 次尝试且输出 FAIL)随下一批落地。
| ...extraEnv, | ||
| }, | ||
| }); | ||
| } catch (e) { |
There was a problem hiding this comment.
[Suggestion] R1-9: Still stands — the two salvage/supersede-cede early exits depend on exiting 0 (a non-zero cede exit would open the failure-fallback gate the cede exists to keep silent), but runScenario still swallows a non-zero exit here — catch (e) { stdout = e.stdout ?? '' } with no status returned — and both cede tests assert only transcript content. Exit 0 is load-bearing behavior with no test coverage: a future edit that turns a cede path into exit 1 (e.g. an errexit-fragile command added before exit 0) ships green — the harness catches the non-zero exit, keeps stdout, and the transcript assertions still pass — while in production the failure-fallback gate opens on the non-zero exit and posts a false failure comment on every legitimate cede.
Fix: return the exit status from runScenario (capture e.status in the catch, return { line, status }) and assert status === 0 in the two cede scenarios. Fix witness: those assertions — a mutant ceding with exit 1 makes them red.
中文说明
[Suggestion] R1-9:仍然存在——salvage/supersede 两处让位提前退出依赖以 0 退出(非零的让位退出会打开让位本要保持沉默的失败兜底门),但 runScenario 在此仍吞掉非零退出——catch (e) { stdout = e.stdout ?? '' },不返回状态——两处让位测试也只断言转录内容。Exit 0 是无测试覆盖的承重行为:未来把让位路径变成 exit 1 的改动(例如在 exit 0 前加入对 errexit 脆弱的命令)会绿色通过——harness 捕获非零退出、保留 stdout,转录断言仍然通过——而在生产中,失败兜底门会因非零退出打开,在每次合法让位上发布虚假的失败评论。
修复:让 runScenario 返回退出状态(在 catch 中捕获 e.status,返回 { line, status }),并在两个让位场景中断言 status === 0。修复验证:这些断言——以 exit 1 让位的变异体会使其变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
There was a problem hiding this comment.
Deferred to the next round under the ~8-finding bound (Criticals first). Agreed exit-0 is load-bearing for the two cede exits; runScenario returning the exit status (and the two cede scenarios asserting status === 0) lands with the next batch.
中文说明
受约 8 条的轮次上限推迟到下一轮(Critical 优先)。认同两处让位退出依赖 exit-0 承重;runScenario 返回退出状态(并在两处让位场景断言 status === 0)随下一批落地。
| // Self-bounded past the budget, and reaped on every exit path — a | ||
| // watcher outliving the step on a reused self-hosted runner could kill | ||
| // a later job's review of the same PR. | ||
| expect(run).toContain('BUDGET_SECONDS + 1800'); |
There was a problem hiding this comment.
[Suggestion] R3-6: The watcher's self-bound — the only protection against a leaked watcher acting on a LATER job of the same PR on the reused self-hosted runner, the exact scenario its own comment above this pin warns about — is pinned by substring only. No executed runWatcher case drives past the bound: every call site stays far under it (max runElapsed used is 12000 vs the 23400s limit at default budget), although the harness makes an over-bound input trivially deliverable. A mutant that no-ops the branch body while leaving the arithmetic text this toContain matches (then return 0 → then :) passes all 26 salvage tests. If such a regression ships, the rare case the bound exists for — a watcher surviving a hard step kill (the trap never ran) — leaves it polling past budget+30min on the shared runner, where its next head-move poll can pkill -f "${REVIEW_URL}" the NEXT job's review of the same PR, with nothing red in CI. The house pattern for every other moving part of this feature is an executed replay.
Witness (mutant run): pristine suite 26 passed; mutant return 0 → : 26 passed (ships green); added over-bound replay runWatcher({ runElapsed: 24000, attemptElapsed: 24000 }) against the mutant FAILS — AssertionError: expected 'head-a' to be null (the unbounded watcher arms the salvage marker past budget) — and passes on the restored clean workflow.
Fix: add one executed case beside the existing runWatcher replays — const expired = runWatcher({ runElapsed: 23401, attemptElapsed: 23401 }); asserting expired.marker/expired.superseded are null and expired.pkilled is false (watcher returns before acting). Fix witness: the added case itself — removing or no-op-ing the self-bound branch in supersede_watcher makes the watcher act past the bound, turning the new assertion red.
中文说明
[Suggestion] R3-6:观察者的自我边界——防止泄漏的观察者在复用的自托管 runner 上作用于同一 PR 的后一个 job 的唯一保护,正是此钉住上方注释警告的场景——仅以子串钉住。没有任何执行的 runWatcher 用例越过边界:所有调用点都远低于它(使用的最大 runElapsed 为 12000,默认预算下边界为 23400 秒),而 harness 本可轻易提供越界输入。把分支体置空但保留该 toContain 匹配的算术文本的变异体(then return 0 → then :)能通过全部 26 个 salvage 测试。若此类回归上线,边界存在的罕见情形——观察者熬过一次硬性步骤杀死(trap 未执行)——会让它在共享 runner 上轮询超过预算 + 30 分钟,其下一次 head 移动轮询可能 pkill -f "${REVIEW_URL}" 掉同一 PR 下一个 job 的评审,而 CI 全绿。本特性其他每个活动部件的惯例都是执行式回放。
验证证据(变异运行):原始套件 26 passed;变异体 return 0 → : 26 passed(绿色通过);新增越界回放 runWatcher({ runElapsed: 24000, attemptElapsed: 24000 }) 对变异体失败——AssertionError: expected 'head-a' to be null(无界观察者在超过预算后仍布防 salvage marker)——在恢复的干净工作流上通过。
修复:在现有 runWatcher 回放旁新增一个执行用例,断言越界时 expired.marker/expired.superseded 为 null 且 expired.pkilled 为 false。修复验证:该用例本身——删除或置空 supersede_watcher 的自我边界分支后必须变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
There was a problem hiding this comment.
Deferred to the next round under the ~8-finding bound (Criticals first). Agreed the self-bound deserves an executed replay rather than a substring pin; the over-bound case (runWatcher({ runElapsed: 23401, attemptElapsed: 23401 }) asserting no marker/no supersede/no pkill) lands with the next batch.
中文说明
受约 8 条的轮次上限推迟到下一轮(Critical 优先)。认同自我边界应以执行式回放钉住而非子串;越界用例(runWatcher({ runElapsed: 23401, attemptElapsed: 23401 }),断言无 marker/无 supersede/无 pkill)随下一批落地。
| expect(delay.env.EVENT_HEAD_SHA).toBe( | ||
| '${{ github.event.pull_request.head.sha }}', | ||
| ); | ||
| expect(delay.run).toContain('while this run queued'); |
There was a problem hiding this comment.
[Suggestion] R1-8: Still stands — the delay job's new stale-head skip, a core gate of this PR, is pinned only by echo strings and the EVENT_HEAD_SHA env: the code under pin is if [ -n "$EVENT_HEAD_SHA" ] && [ -n "$current_head" ] && [ "$current_head" != "$EVENT_HEAD_SHA" ] in delay-automatic-review's pr_state step, and an operator flip ships green — the mutant != → = passes all 200 tests (both substrings survive, and should_review=false also appears in the pre-existing OPEN/draft branches, so it does not even identify the head-move path). Under that mutant, every lifecycle run whose head did NOT move while queued — the common case for synchronize — emits should_review=false and silently skips the automatic review, all checks green; flipped the other way, dead-head runs return (the #10110 regression this PR fixes). The diff's own standard for blocks outside the retry-loop extraction window is executed extraction — the salvage outputs and percent parse are replayed, not shape-pinned; this guard is the one new executable left at shape-pin level.
Witness (mutant run): baseline Tests 200 passed (200); mutant != → = Tests 200 passed (200) — mutation survives.
Fix: extract the pr_state run block and execute it like runSalvageOutputs does — stub gh to answer pr view with OPEN<TAB>false<TAB><head> and run two cases: live head equal to EVENT_HEAD_SHA (expect should_review=true) and moved (expect should_review=false plus the while this run queued summary line). Fix witness: the executed replay — the unmoved-head case must assert should_review=true, which is the assertion the != → = mutation fails.
中文说明
[Suggestion] R1-8:仍然存在——delay job 新增的过期 head 跳过(本 PR 的核心闸门)只被 echo 字符串与 EVENT_HEAD_SHA 环境变量钉住:被钉住的代码是 delay-automatic-review 的 pr_state 步骤中的 if [ -n "$EVENT_HEAD_SHA" ] && [ -n "$current_head" ] && [ "$current_head" != "$EVENT_HEAD_SHA" ],操作符翻转可以绿色通过——变异体 != → = 通过全部 200 个测试(两个子串都保留,且 should_review=false 也出现在既有的 OPEN/draft 分支中,甚至无法识别 head 移动路径)。在该变异体下,每个排队期间 head 未移动的生命周期运行——synchronize 的常见情形——都会输出 should_review=false 并无声跳过自动评审,所有检查绿色;反向翻转则让死 head 运行回归(即本 PR 要修复的 #10110 回归)。本 diff 对重试循环提取窗口之外代码块的标准是执行式提取——salvage 输出与百分比解析都是回放而非形状钉住;该守卫是唯一仍停留在形状钉住层的新增可执行逻辑。
验证证据(变异运行):基线 Tests 200 passed (200);变异体 != → = Tests 200 passed (200)——变异存活。
修复:像 runSalvageOutputs 一样提取并执行 pr_state 运行块——stub gh 以 OPEN<TAB>false<TAB><head> 应答 pr view,运行两个用例:live head 等于 EVENT_HEAD_SHA(断言 should_review=true)与已移动(断言 should_review=false 及 while this run queued 摘要行)。修复验证:该执行式回放——未移动 head 用例必须断言 should_review=true,这正是 != → = 变异体失败的断言。
— qwen3.8-max via Qwen Code /review (v0.22.2)
There was a problem hiding this comment.
Deferred to the next round under the ~8-finding bound (Criticals first). Agreed the delay job's stale-head skip is the one new executable still at shape-pin level; extracting and executing the pr_state block like runSalvageOutputs (unmoved case asserting should_review=true, moved case asserting should_review=false plus the summary line) lands with the next batch.
中文说明
受约 8 条的轮次上限推迟到下一轮(Critical 优先)。认同 delay job 的过期 head 跳过是唯一仍停留在形状钉住层的新增可执行逻辑;像 runSalvageOutputs 一样提取并执行 pr_state 块(未移动用例断言 should_review=true,已移动用例断言 should_review=false 及摘要行)随下一批落地。
# Conflicts: # .github/workflows/.size-baseline # .github/workflows/qwen-autofix.md
…aware Round-3 review fixes for the supersede-salvage mechanism (#10110): every signal write goes aside-then-rename (write_signal) and every read gates on [ -f ], so planted FIFOs at the agent-derivable signal paths can no longer block the watcher, the finished step, or the posting path; the compose latch refuses non-regular artifacts; the cede message validates the supersede target to the watcher's 40-hex shape before interpolation; the salvage outputs block re-checks the live head like the cede sites; the post-attempt supersede check carries the OUTCOME guard its sibling has; and the per-attempt reset also runs before the retry backoff. The replay harness pins AUTO_REVIEW off so an exporting environment cannot arm the watcher inside a replay. Each guard has an executed replay witness, all mutation-probed red.
|
🤖 Addressed the latest review feedback (round 3/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 3/100 轮)。改动内容与我反驳保留之处如下: Round summary — PR #10123 (review-hold-and-salvage)Growth audit (required this round)This window is over its growth budget (source 80 / test 447 net lines vs Base conflict resolution (--conflict true)Merged
Feedback dispositionsAddressed (8 — all 5 Criticals first, then the 3 highest-defect Suggestions;
Deferred to the next round (6 — the ~8-finding bound, Criticals first; all The CHANGES_REQUESTED review body (rv:5036204370) carries no actionable VerificationCommands actually run, all from the repository root, all passed:
中文说明轮次总结 — PR #10123(review-hold-and-salvage)增长审计(本轮必需)本窗口超出增长预算(相对基线净增长:源码 80 / 测试 447 行,预算 400/400)。审计在任何编辑之前执行; 基线冲突解决(--conflict true)已合并
反馈处置已处理(8 条——全部 5 条 Critical 优先,随后是缺陷等级最高的 3 条 Suggestion;本轮以约 8 条为上限):
推迟到下一轮(6 条——受约 8 条的轮次上限约束,Critical 优先;均为建议级见证加固,或需要单独一轮谨慎处理的攻击面变更,且推迟有助于在窗口超标期间控制测试增长):R2-10(rc:3867787974)、R2-8(rc:3867787978)、R3-4(rc:3867787989)、R1-9(rc:3867787994)、R3-6(rc:3867787997)、R1-8(rc:3867788000)。每条均已通过 CHANGES_REQUESTED 评审主体(rv:5036204370)除行内发现外无可执行条目(仅披露审查缺口与收敛观察)。无失败或持续失败的检查。 验证以下命令均在仓库根目录实际执行且通过:
Deferred non-Critical feedbackCritical-only mode is active: the PR's diff grew src 80 / test 447 net lines beyond this counting window's baseline (budgets: 400/400). The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback continues to flow unaffected during a growth-only engagement (the per-author batch budget applies only after 5 change-producing rounds). ( 中文说明已进入仅处理 Critical 的模式:本计数窗口内 diff 净增长已达 源码 80 / 测试 447 行(预算 400/400)。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。纯增长触发期间维护者反馈照常流动(按作者的批次预算仅在完成 5 个产生改动的轮次后生效)。(评论 Base-conflict check · 基分支冲突检查: conflicted with main — resolved in this push. · 与 main 有冲突——已在本次推送中解决。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: reverse audit — stopped at the 5-round cap without converging (round 5 still reported findings).
Not explored to full depth (tool budget reached): chunk 6: full vitest run of scripts/tests/qwen-resolve-workflow.test.js (dependencies not installed in the review worktree; compensated by directly executing the three….
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted; the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
Deferred under the convergence posture (round 4, not a blocker) — recorded, not requested in this round:
.github/workflows/qwen-autofix.md:3785 — [review] hold coverage incomplete vs #10110 half 1 (fleet-shepherd sync).github/workflows/qwen-autofix.md:1102 — [review] af-028 rationale describes the removed synchronize-cancelscripts/tests/qwen-autofix-workflow.test.js:21757 — [review] hold tests pin jq filters but not the ROLLUP_R/LIVE_HEAD_R fetchesscripts/tests/qwen-autofix-workflow.test.js:21824 — [review] runs-API fallback replay exercises only status 'waiting'scripts/tests/qwen-pr-review-workflow.test.js:4137 — [review] watcher CEDE kill sequence (TERM→15s→KILL) has no behavioral witness.github/workflows/qwen-code-pr-review.yml:1355 — [review] SALVAGE_DIR setup/export producer lines pinned by no test
Convergence: round 4 posted 12 inline comment(s), 7 of them reported for the first time; the previous round posted 14 (9 new). Findings keep coming back to the same files: .github/workflows/qwen-code-pr-review.yml (findings in rounds 1, 2, 3; 6 more now); scripts/tests/qwen-pr-review-workflow.test.js (findings in rounds 1, 3; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)
中文说明
仅完成部分审查,审查缺口已披露。
未审查:reverse audit — stopped at the 5-round cap without converging (round 5 still reported findings)。
未探索到全部深度(达到工具调用预算):chunk 6:full vitest run of scripts/tests/qwen-resolve-workflow.test.js (dependencies not installed in the review worktree; compensated by directly executing the three…。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted; the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
收敛姿态下延后(第 4 轮,非阻断)——已记录,本轮不要求修改:共 6 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 4 轮发布了 12 条行内评论,其中 7 条是首次提出;上一轮发布了 14 条(其中 9 条首次提出)。发现反复回到同一批文件:.github/workflows/qwen-code-pr-review.yml(第 1、2、3 轮已出过发现,本轮又有 6 条);scripts/tests/qwen-pr-review-workflow.test.js(第 1、3 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)
— qwen3.8-max via Qwen Code /review (v0.22.2)
| if [ "$OUTCOME" != "success" ] && [ -f "${SUPERSEDE_FILE:-}" ] && live_head_moved; then | ||
| cede_superseded | ||
| fi |
There was a problem hiding this comment.
[Critical] R4-1: A watcher cede-kill followed by a fast head revert — or a transient gh failure at this re-check — falls through to a fatal failure report (red job plus fallback failure comment) instead of the clean cede this check exists to produce. The watcher writes SUPERSEDE_FILE and kills the tree (exit 143 → OUTCOME=fatal, 137 → timeout); if the push is force-reverted to the reviewed head in the ~20-40s before this re-check, live_head_moved returns false and the cede is skipped; a kill is never classified retryable, so the loop reaches fail() and goes red — and the in-job fallback's own head-moved guard does NOT skip because the head equals EXPECTED_HEAD_SHA again, so a failure comment posts on a run that deliberately superseded itself and whose queued replacement already covers the head.
Witness (probe over the extracted production loop): SUPERSEDE_FILE present + fatal attempt, STUB_LIVE_HEAD=head-a (reverted) → FAIL kind=[] reason=[Qwen review exited with status 3.]; STUB_LIVE_HEAD='' (gh failure) → identical; control arm STUB_LIVE_HEAD=head-b → Superseded early: … ceding to the replacement run (#10110), no FAIL.
Verify the cede's premise against an unforgeable source before failing: when OUTCOME != success and SUPERSEDE_FILE exists but the head reverted, check the PR timeline (gh api repos/$REPO/issues/$PR_NUMBER/timeline) for a synchronize/force-push entry matching the file's recorded SHA, and cede_superseded on a match; keep the red failure only when that verification is unavailable or contradicts the file.
Fix witness: a retry-loop replay where the attempt ends with the watcher-kill signature (exit 143), SUPERSEDE_FILE present, live head == expected head, and the recorded SHA verified as a former head → expect cede (exit 0, 'Superseded early'), not fail; removing the verified-cede branch must turn it red.
中文说明
[Critical] R4-1:观察者(watcher)让位杀树之后如果 head 快速回退——或重查时 gh 瞬时失败——会穿透到致命失败上报(红色 job + 兜底失败评论),而不是本检查本应给出的干净让位。观察者写出 SUPERSEDE_FILE 并杀树(退出码 143 → OUTCOME=fatal,137 → timeout);若 push 在重查前约 20-40 秒内被强制回退到被评审 head,live_head_moved 返回 false、让位被跳过;kill 永远不会被归类为可重试,循环走到 fail() 变红——而 job 内兜底自己的 head 移动守卫此时因 head 重新等于 EXPECTED_HEAD_SHA 而不跳过,于是在一次刻意自我让位的 run 上发布失败评论(排队的接替 run 已覆盖该 head)。
验证证据(对提取的生产 loop 做探针):有 SUPERSEDE_FILE + 致命尝试、STUB_LIVE_HEAD=head-a(已回退)→ FAIL;STUB_LIVE_HEAD=''(gh 失败)→ 相同;对照臂 head-b → Superseded early: …,无 FAIL。
建议:失败前先向不可伪造的数据源核验让位前提——当 OUTCOME != success 且 SUPERSEDE_FILE 存在但 head 已回退时,查 PR timeline 中是否存在与文件所记 SHA 吻合的 synchronize/强推记录,吻合则 cede_superseded;仅在无法核验或核验矛盾时保留红色失败。
修复验证:新增回放——尝试以 watcher-kill 特征结束、SUPERSEDE_FILE 存在、live head 等于 expected head、所记 SHA 经核验为历史 head → 断言让位(exit 0)而非失败;删除该校验分支后必须变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| reset_attempt_salvage_state() { | ||
| rm -f "${SALVAGE_DIR}/compose-seen" "${COMPOSED_ARTIFACT:-}" | ||
| write_signal "${SALVAGE_DIR}/attempt-start" "$(date +%s)" | ||
| } |
There was a problem hiding this comment.
[Critical] R4-2: Salvage state is only half per-attempt: this reset clears the compose latch and artifact but not the armed marker, moved-to, or superseded signals, and the one-shot supersede_watcher (which returns the moment it arms or cedes) is never restarted for the retry — so a retried attempt runs with the previous attempt's signals and no watcher at all, contrary to the fresh-state invariant this hunk's comment states. Witness (probe over the extracted loop): planted marker (salvage-ok=head-a) + superseded=head-b, retryable attempt, reverted head → attempts == 2 with BOTH signal files surviving the reset contents-intact, run ends FAIL; no KEEP/CEDE decision fires during attempt 2.
Concrete harm: during attempt 2 the head moves and nothing polls it — attempt 2 burns the entire remaining shared budget re-reviewing a dead head while the queued replacement waits; if it completes, the guard's salvage escape admits the post on attempt 1's stale marker (a historical-head post the threshold policy says a fresh sub-threshold attempt must not get); in the cede-then-retry arm the head-moved guard instead blocks the post → attempts exhausted → fail → hours of completed review discarded — the exact #9729 loss shape this PR exists to eliminate.
# on the retry branch, after reset_attempt_salvage_state:
if [ "${AUTO_REVIEW:-false}" = "true" ] && ! kill -0 "$WATCHER_PID" 2>/dev/null; then
supersede_watcher & WATCHER_PID=$!
fiRestart the watcher when the previous one is gone (the per-run self-bound and the fresh attempt-start already make a relaunched watcher correct); consider also clearing moved-to/marker/superseded in the reset symmetrically with the latch — every cede site's live-head re-check already makes the cleared files safe.
Fix witness: a replayed-loop case — attempt 1 ends retryable with SUPERSEDE_FILE present and the head reverted (retry proceeds, watcher dead), head moves early in attempt 2 → expect cede within one poll interval instead of running attempt 2 out; removing the relaunch must turn it red.
中文说明
[Critical] R4-2:salvage 状态只做到了一半的「按尝试」:该重置只清除 compose 锁存与工件,不清除已布防的 marker、moved-to、superseded 信号;且一次性的 supersede_watcher(布防或让位即返回)在重试时从不重启——重试尝试带着上一尝试的信号、且完全没有观察者地运行,违反本代码块注释自己声明的「全新状态」不变量。验证证据(探针对提取 loop):放置 marker + superseded、可重试失败、head 回退 → attempts == 2 且两个信号文件完整存活于重置之后,run 以 FAIL 结束;尝试 2 期间无任何 KEEP/CEDE 决策。
具体危害:尝试 2 期间 head 移动却无人轮询——尝试 2 烧掉整个剩余共享预算重审死 head,接替 run 干等;若其完成,guard 逃生口凭尝试 1 的陈旧 marker 放行发布(阈值策略不允许的历史 head 发布);在「让位后重试」臂则相反被 head 移动守卫挡住 → 次数耗尽 → fail → 数小时已完成评审被丢弃——正是本 PR 要消除的 #9729 损失形态。
建议:重试分支上观察者已死则重启(上方代码);重置亦可对称清除 marker/moved-to/superseded——各让位点的 live-head 复查已使清除后的文件安全。
修复验证:新增回放——尝试 1 可重试结束且 SUPERSEDE_FILE 存在、head 回退(重试继续、观察者已死),尝试 2 早期 head 移动 → 断言一个轮询间隔内让位而非跑完尝试 2;删除重启逻辑后必须变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| if [ "$DOCS_ONLY_MEDIUM" != "true" ] \ | ||
| && salvage_eligible "$elapsed" "$BUDGET_SECONDS" "$compose_seen" "$SALVAGE_ELAPSED_PERCENT"; then |
There was a problem hiding this comment.
[Critical] R4-3: The salvage threshold mixes a per-attempt elapsed numerator with the full run budget as denominator, so a retry attempt that starts with less than SALVAGE_ELAPSED_PERCENT of the budget remaining can never reach the elapsed KEEP branch — its late stages are CEDE-killed minutes from posting. The comment block above this hunk says eligibility 'measures the work being discarded — the current attempt's, not the run's'; the denominator contradicts exactly that. Trigger: attempt 1 dies retryably after consuming 55% of a 360-minute budget; attempt 2 starts with ~162 min left (attempt_timeout clamps it to the remainder); when the head moves after attempt 2 has burned 135 min — 83% of the only budget it ever had, compose not latched — the check computes 8100×100 = 810000 < 21600×50 = 1080000 → CEDE: pkill kills the attempt minutes from posting and the replacement restarts from zero, discarding attempt 1 + attempt 2 — the exact #9729 shape. Whenever attempt 1 consumes more than (100 − pct)% of the budget before a retryable failure, the elapsed branch is structurally unreachable for attempt 2; only the compose latch can save it.
Witness (probe over the extracted watcher, budget=21600, pct=50, head moved):
late-attempt2@8100s (84% of its 9660s budget): decision=CEDE superseded=head-b marker=null pkilled=true
late-attempt2@9659s (~100% of its budget): decision=CEDE superseded=head-b marker=null pkilled=true
boundary: elapsed=10800 → KEEP; attempt 2 dies at 9660s → unreachable
FIXED (attempt-budget denominator): 8100s → KEEP marker=head-a; controls unchanged (fresh-attempt2@30s → CEDE)
Record the attempt's budget at reset — derivable in the watcher as attempt_budget = BUDGET_SECONDS - (attempt_start - START_TS) from the same attempt-start file — and pass that as the denominator instead of $BUDGET_SECONDS.
Fix witness: extend the replayed-watcher suite with runWatcher({ runElapsed: 20000, attemptElapsed: 8000 }) (attempt 1 spent 12000s of 21600s; attempt 2 has spent 83% of its 9600s remainder) asserting marker === 'head-a'; under the current code it returns CEDE, so the test is red today and red again if a per-attempt-denominator fix is reverted.
中文说明
[Critical] R4-3:salvage 阈值把「按尝试」的已耗时(分子)与整个 run 的预算(分母)混用:剩余预算不足 SALVAGE_ELAPSED_PERCENT 的重试尝试永远到不了按耗时的 KEEP 分支——其尾声会在距发布几分钟时被 CEDE 杀掉。上方注释明确说资格判定「衡量将被丢弃的工作——当前尝试的,而非 run 的」;分母恰与之矛盾。触发:尝试 1 在消耗 360 分钟预算的 55% 后可重试死亡;尝试 2 仅剩约 162 分钟;当尝试 2 烧掉 135 分钟(其唯一预算的 83%,compose 未锁存)时 head 移动,计算得 810000 < 1080000 → CEDE:距发布几分钟被杀,接替 run 从零重来,尝试 1 + 尝试 2 一并丢弃——正是 #9729 形态。只要尝试 1 在可重试失败前消耗超过 (100 − pct)% 预算,尝试 2 的按耗时分支结构性不可达,只剩 compose 锁存能救。
验证证据(探针):见上方双臂输出——现状 8100s/9659s 均 CEDE;改用「尝试预算」作分母后翻转为 KEEP。
建议:重置时记录本尝试的预算(观察者内可由 BUDGET_SECONDS - (attempt_start - START_TS) 推导),以它替代 $BUDGET_SECONDS 作分母。
修复验证:新增 runWatcher({ runElapsed: 20000, attemptElapsed: 8000 }) 断言 marker === 'head-a';现状代码该测试为红,回退按尝试分母的修复后再次变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| if [ "$OUTCOME" != "success" ] && [ -f "${SUPERSEDE_FILE:-}" ] && live_head_moved; then | ||
| cede_superseded | ||
| fi | ||
| if [ "$OUTCOME" != "success" ] && [ -f "${QWEN_CI_REVIEW_SALVAGE_OK_FILE:-}" ] && live_head_moved; then |
There was a problem hiding this comment.
[Critical] R4-4: A head move that lands inside the watcher's ≤60s poll gap ends this run as a red FAIL even though it was genuinely superseded. This diff removes synchronize from cancel-in-progress, making the poll watcher plus these file-gated checks the only supersede path; when the head moves at T and the agent's gh write lands at T+ε — before the watcher's next poll — guard_pr_write sees a moved head with no marker and exits 90 (OUTCOME=fatal). Neither post-run cede check fires because no signal file exists yet (the EXIT trap reaps the watcher before its next poll), and the terminal fail path never consults the live head — so the job goes red with 'Qwen review exited with status 90.' while a replacement run is already queued. Both fallback paths skip moved heads, so the observable damage is a spurious red run exactly in the near-post window the salvage machinery exists to protect. Distinct from R4-1: there the watcher had acted; here it never did.
Witness (probe over the extracted loop, head MOVED to head-b, no signal files):
A1 qwen exits 90, current code: RED JOB (exit 1) FAIL reason=[Qwen review exited with status 90.]
A2 retryable x2 (attempt 2 = MAX): RED JOB (exit 1) FAIL reason=[Qwen review aborted with an API error…]
B1/B2 + `if live_head_moved; then cede_superseded; fi` before fail: CLEAN CEDE (exit 0) 'Superseded early: …'
C1 control, head UNMOVED + fix: RED JOB (exit 1) — no spurious cede
Before the terminal fail in the attempt loop, cede when the head really moved: if live_head_moved; then cede_superseded; fi — the live-head re-read is already this diff's own answer to forgeable markers.
Fix witness: a replayed-loop scenario where the stubbed attempt exits 90 with STUB_LIVE_HEAD=head-b and no signal files planted → assert one attempt, output contains 'Superseded early:' and no 'FAIL '; today the loop falls through to fail, so the test is red until the guard is added and red again if it is removed.
中文说明
[Critical] R4-4:落在观察者 ≤60 秒轮询间隙内的 head 移动,会让一次确实已被取代的 run 以红色 FAIL 收场。本 diff 把 synchronize 移出 cancel-in-progress,轮询观察者 + 这些文件门控检查成为唯一的取代路径;head 在 T 时刻移动、agent 的 gh 写入在 T+ε(观察者下次轮询之前)到达时,guard_pr_write 见到移动且无 marker → exit 90(OUTCOME=fatal)。两个尝试后让位检查都不会触发(尚无信号文件——EXIT trap 已在下次轮询前回收观察者),而终局 fail 路径从不重查 live head——于是 job 变红、报错「status 90」,尽管接替 run 已在排队。两个兜底都会跳过已移动的 head,因此可观察损害是一次恰好落在「临近发布」窗口(salvage 机制本要保护的窗口)的假红色。与 R4-1 不同:那里观察者动过;这里它从未动过。
验证证据(探针):见上方 A1/A2(红)与 B1/B2(加 live_head_moved 检查后干净让位)、C1 对照。
建议:在尝试循环的终局 fail 之前,若 head 确实移动则让位:if live_head_moved; then cede_superseded; fi。
修复验证:新增回放——尝试以 exit 90 结束、STUB_LIVE_HEAD=head-b、不放置任何信号文件 → 断言一次尝试且输出含 'Superseded early:' 无 'FAIL ';加上守卫前该测试为红,删除后再次变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| if [ -n "${QWEN_CI_REVIEW_SALVAGE_OK_FILE:-}" ] \ | ||
| && [ -f "${QWEN_CI_REVIEW_SALVAGE_OK_FILE}" ] \ | ||
| && [ "$(cat "${QWEN_CI_REVIEW_SALVAGE_OK_FILE}" 2>/dev/null)" = "$expected_head" ]; then |
There was a problem hiding this comment.
[Suggestion] R4-5: This marker read is the one salvage-signal read with no planted-FIFO test, so its [ -f ] gate — which the comment above says keeps a planted FIFO from blocking the posting path 'forever' — is unpinned. Every sibling signal site (superseded, moved-to, attempt-start, compose-seen, composed.json) has an explicit FIFO-plant test; this one has none. A mutant replacing [ -f ] with [ -e ] (or dropping the gate) leaves all three existing salvage wrapper cases green, since they all plant regular files; with a FIFO at the marker path — derivable from the exported QWEN_CI_REVIEW_SALVAGE_OK_FILE — the ungated cat hangs the posting path indefinitely on the next head-drifted write.
Witness (probe in scratch tree): mutant [ -f ] → [ -e ], targeted vitest → still passes (1 passed | 29 skipped); verbatim-extracted wrapper with a mkfifo at the marker path and a head-drifted write, bounded by timeout 10 → PRISTINE: exit=90 'Blocked PR write' immediately; MUTANT: exit=124 (hit the 10s bound — the ungated cat hangs).
Add a fourth case to 'lets a salvage-armed run post against its reviewed head after a move' in scripts/tests/qwen-resolve-workflow.test.js: create the marker path with mkfifo instead of writeFileSync, expect status 90 and 'Blocked PR write', and give the harness's spawnSync a timeout (runScenario added a 30s bound for exactly this hazard) so a regression fails fast instead of hanging.
Fix witness: the new FIFO case itself — removing the [ -f ] gate here must make it red (hang into the bound).
中文说明
[Suggestion] R4-5:该 marker 读取是唯一没有「放置 FIFO」测试的 salvage 信号读取,因此其 [ -f ] 闸门(上方注释称其防止放置的 FIFO 永久阻塞发布路径)未被钉住。其余每个信号站点都有显式 FIFO 放置测试。将 [ -f ] 换成 [ -e ](或删除闸门)的变异体保持全部三个现有用例绿色(它们放置的都是普通文件);在 marker 路径(可由导出的 QWEN_CI_REVIEW_SALVAGE_OK_FILE 推导)放置 FIFO 时,无闸门的 cat 会在下一次 head 漂移写入时无限期挂起发布路径。
验证证据:变异体 [ -f ]→[ -e ] 后定向 vitest 仍通过;提取的 wrapper + mkfifo + 漂移写入,限超时 10 秒:现状立即 exit=90,变异体 exit=124(挂起)。
建议:在 resolve 套件新增 mkfifo 用例,断言 90 + 'Blocked PR write',并给 spawnSync 加超时。
修复验证:删除此处 [ -f ] 闸门后新用例必须变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
There was a problem hiding this comment.
Deferred to the next round: this round's batch was bounded to the four Criticals plus the highest-priority witnesses, and this FIFO-plant witness for the guard's marker read is a test-only addition to the resolve suite. The [ -f ] gate it would pin is unchanged by this commit and still covered by the shape of the sibling signal tests.
延后到下一轮:本轮批次限定在四个 Critical 与优先级最高的见证上,该针对守卫 marker 读取的 FIFO 放置见证是对 resolve 套件的纯测试增补。它所要钉住的 [ -f ] 闸门本次提交未改动,且仍由同类信号测试的形状覆盖。
| expect(delay.run).toContain('while this run queued'); | ||
| expect(delay.run).toContain('should_review=false'); |
There was a problem hiding this comment.
[Suggestion] R1-8: Still stands — the delay job's stale-head skip, a core gate of this PR, remains pinned only by echo strings and the EVENT_HEAD_SHA env: this test never constrains the comparison direction, so an inverted condition ships green. A future edit flipping [ "$current_head" != "$EVENT_HEAD_SHA" ] to = makes every lifecycle run whose head has NOT moved — the normal case — emit should_review=false, silently disabling automatic review while stale-head runs proceed; all current assertions survive ('while this run queued' stays in the step text, and 'should_review=false' also appears in the draft/MERGEABLE branches), and the delay job gates the whole chain, so there is no runtime backstop. The house pattern for the sibling guard already exists: qwen-resolve-workflow.test.js pins the review-pr job's head-drift condition verbatim.
Witness (probe): mutant != → = applied, vitest -t 'skips a queued run whose event head went stale' → still passes (1 passed | 205 skipped); behavioral A/B driving the extracted delay step with OPEN, not draft, head UNMOVED → MUTANT: should_review=false (every normal lifecycle run silently skipped); PRISTINE: should_review=true.
Pin the condition verbatim — expect(delay.run).toContain('[ "$current_head" != "$EVENT_HEAD_SHA" ]') — or extract the branch and replay it as the salvage-outputs block is replayed: same head → no skip; moved head → should_review=false plus the 'while this run queued' summary line.
Fix witness: the new verbatim/replayed assertion — inverting the operator in the workflow's pr_state step must turn it red.
中文说明
[Suggestion] R1-8:仍然存在——delay job 的过期 head 跳过是本 PR 的核心闸门,但依旧只被 echo 字符串与 EVENT_HEAD_SHA 环境变量钉住:该测试从不约束比较方向,条件翻转也能绿色发布。把 [ "$current_head" != "$EVENT_HEAD_SHA" ] 翻成 = 会使所有 head 未移动的生命周期 run(常态)输出 should_review=false,静默关闭自动评审;现有断言全部存活,且 delay job 门控整条链路,无运行时兜底。
验证证据:变异体 !=→= 后定向测试仍通过(1 passed | 205 skipped);行为 A/B:未移动 head 时变异体 should_review=false、现状 true。
建议:逐字钉住比较式,或提取该分支回放(同 head 不跳过;移动 head → should_review=false + 摘要行)。
修复验证:翻转工作流中的运算符后新断言必须变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| && node -e 'const v=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));if(typeof v!=="object"||v===null||Array.isArray(v))process.exit(1);' "$COMPOSED_ARTIFACT" 2>/dev/null; then | ||
| write_signal "${SALVAGE_DIR}/compose-seen" '' | ||
| fi | ||
| live_head="$(gh pr view "$PR_NUMBER" --repo "$REPO" --json headRefOid --jq '.headRefOid' 2>/dev/null)" || continue |
There was a problem hiding this comment.
[Suggestion] R2-8: Still stands — this || continue tolerance of a failed gh pr view poll is the only thing keeping a transient API error from silently killing the entire supersede mechanism, and no test exercises it: every replayed gh stub in the salvage suite exits 0 unconditionally (runScenario, runWatcher, runSalvageOutputs). Under the step's inherited set -euo pipefail, deleting || continue turns any single failed poll into a silent death of the background watcher subshell. A lifecycle review polls every 60s for 3-6h (180-360 calls); one transient failure — a secondary rate limit on the shared bot token, a 5xx, a network blip — is routine at that volume. With the guard gone the watcher dies silently; if the head later moves, the run loses both halves of #10110: no CEDE (the queued successor waits out the entire remaining budget instead of one poll interval) and no salvage marker (guard_pr_write blocks the post on the moved head, exit 90) — hours of finished review discarded, the exact PR #9729 loss class.
Witness (probe flip over the extracted production watcher, gh stub fails its first invocation then serves the moved head): intact guard → superseded=head-b, pkill logged, exit 0; mutant with || continue removed → no decision, no pkill, exit 1 (silent watcher death under errexit).
Add a runWatcher case whose gh stub fails its first invocation and then serves the head (a counter file: first call exit 1, later calls echo head-b), asserting the one-shot decision still lands (superseded === 'head-b', pkilled true) inside the existing 30s bound — the harness already runs supersede_watcher under set -euo pipefail, reproducing production errexit semantics exactly.
Fix witness: the new replay itself — with || continue removed from this line, the harness exits non-zero on the first failing poll and the superseded/pkill assertions fail.
中文说明
[Suggestion] R2-8:仍然存在——该 || continue 对 gh pr view 轮询失败的容忍是整个取代机制唯一的容错屏障,却没有任何测试覆盖:salvage 套件中所有回放 gh stub 都无条件 exit 0。在步骤继承的 set -euo pipefail 下,删除 || continue 会让任何单次轮询失败静默杀死后台观察者子 shell。生命周期评审每 60 秒轮询一次、持续 3-6 小时(180-360 次调用),一次瞬时失败(共享 bot token 的二级限流、5xx、网络抖动)在该量级下属常态。守卫消失后观察者静默死亡;此后 head 移动时,run 同时失去 #10110 的两半:无 CEDE(接替 run 等满整个剩余预算)、无 salvage marker(发布被 exit 90 阻止)——数小时已完成评审被丢弃。
验证证据(探针翻转):守卫完好 → 决策落地、pkill 记录、exit 0;删除 || continue 的变异体 → 无决策、无 pkill、exit 1。
建议:新增 runWatcher 用例:gh stub 首次调用失败、之后返回 head,断言一次性决策仍落地。
修复验证:删除本行的 || continue 后,新回放在首个失败轮询处非零退出且断言失败。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| // Self-bounded past the budget, and reaped on every exit path — a | ||
| // watcher outliving the step on a reused self-hosted runner could kill | ||
| // a later job's review of the same PR. | ||
| expect(run).toContain('BUDGET_SECONDS + 1800'); |
There was a problem hiding this comment.
[Suggestion] R3-7: Still stands — the watcher's self-bound expiry branch (past budget + 30 minutes, return without acting), the only protection against a leaked watcher acting on a LATER job of the same PR on the reused self-hosted runner, remains pinned only as a string; no replay drives run_elapsed past the bound. All runWatcher cases use runElapsed 60 or 12000, both inside the 21600+1800 bound; a mutant replacing return 0 with continue (or deleting the branch) keeps the string pin and every existing replay green. The branch exists precisely for the scenario its comment names — a watcher leaked through a hard step kill — which would then keep polling past the budget and could pkill the NEXT job's review of the same PR on the reused runner.
Witness (probe): mutant return 0 → continue applied, the whole review supersede salvage (#10110) describe block → 30 passed, the mutant survives the entire suite; behavioral A/B driving the extracted watcher with run_elapsed=25000s (> bound 23400s) and a moved head → PRISTINE: exit 0 immediately, superseded=ABSENT marker=ABSENT pkilled=false; MUTANT: never terminates — ETIMEDOUT at the 30s harness bound.
Add a runWatcher case with runElapsed past the bound (e.g. runElapsed: 23401, budget: 21600) and a moved head, expecting { marker: null, movedTo: null, superseded: null, pkilled: false } — the harness already sets START_TS/BUDGET_SECONDS to drive exactly this.
Fix witness: the new case — removing the self-bound's return 0 in supersede_watcher must turn it red.
中文说明
[Suggestion] R3-7:仍然存在——观察者的自我界限过期分支(超过预算 + 30 分钟即返回不行动)是防止泄漏的观察者作用于同一 PR 后续 job 的唯一保护(复用自托管 runner 场景),却仍只被字符串钉住;没有任何回放把 run_elapsed 驱过界限。所有 runWatcher 用例使用 60 或 12000,均在界限内;把 return 0 换成 continue(或删除分支)的变异体保持字符串钉住与全部回放绿色。该分支正是为其注释所述场景(硬杀步骤后泄漏的观察者)而存在——届时会继续轮询越过预算,并可能 pkill 同一 PR 下一个 job 的评审。
验证证据:变异体 return 0→continue 后 30 个测试全过;行为 A/B:run_elapsed=25000s 时现状立即退出、变异体永不终止(30 秒上限被触发)。
建议:新增越界 runWatcher 用例(如 runElapsed: 23401)+ 移动 head,断言无任何行动。
修复验证:删除自我界限的 return 0 后新用例必须变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| SALVAGE_DIR="$(mktemp -d "${RUNNER_TEMP:-/tmp}/qwen-review-salvage.XXXXXX")" | ||
| export QWEN_CI_REVIEW_SALVAGE_OK_FILE="${SALVAGE_DIR}/salvage-ok" |
There was a problem hiding this comment.
[Suggestion] R2-10: Still stands — the salvage escape applies to every run, but the marker it trusts is only ever legitimately armed by the supersede watcher, which is gated on AUTO_REVIEW=true; this setup/export is unconditional. In explicit runs (/review comments, review_request, dispatch) the escape can therefore fire only via forgery: the agent derives SALVAGE_DIR from its own exported environment, writes a salvage-ok marker with content = expected_head_sha (also exported), and after any later head move the guard admits the post. The escape's comment documents 'a correctness gate, not a security boundary', but in explicit runs there is no legitimate arming path at all — the residual-forgery risk the comment accepts for lifecycle runs is the ONLY path here, and the unconditional export widens the agent-derivable surface the rest of this machinery defends against.
Gate the SALVAGE_DIR setup/export on [ "${AUTO_REVIEW:-false}" = "true" ] (watcher-only arming), and add the :- default on the post-loop output read ($QWEN_CI_REVIEW_SALVAGE_OK_FILE at the post-loop block) since the step runs set -u — the dev-bot's own round-2 reply noted this implementation detail.
Fix witness: a shape pin asserting the SALVAGE_DIR/export block sits inside the AUTO_REVIEW gate, plus a replayed-guard case asserting an explicit-run env (no export) still blocks a moved-head write with exit 90 even when a marker file exists at a supplied path.
中文说明
[Suggestion] R2-10:仍然存在——salvage 逃生口对所有 run 生效,但其信任的 marker 只可能由受 AUTO_REVIEW=true 门控的取代观察者合法布防;此处的 setup/export 无条件执行。显式 run(/review 评论、review_request、dispatch)中,逃生口只能经由伪造触发:agent 从自己导出的环境推导 SALVAGE_DIR,写入内容等于 expected_head_sha(同样已导出)的 marker,之后任何 head 移动都会让守卫放行发布。逃生口注释声明「正确性闸门,非安全边界」,但显式 run 根本不存在合法布防路径——注释为生命周期 run 接受的「残余伪造风险」在这里是唯一路径;无条件导出还扩大了本机制其余部分处处设防的 agent 可推导面。
建议:将 SALVAGE_DIR 的 setup/export 门控在 AUTO_REVIEW=true 上;loop 后输出读取处补 :- 默认值(步骤运行于 set -u,dev-bot 第 2 轮回复已提示该细节)。
修复验证:形状钉住(setup 位于 AUTO_REVIEW 门内)+ 回放守卫用例(显式 run 环境即使存在 marker 仍 exit 90)。
— qwen3.8-max via Qwen Code /review (v0.22.2)
There was a problem hiding this comment.
Deferred to the next round: gating the SALVAGE_DIR setup/export on AUTO_REVIEW is a production behavior change for explicit runs and needs the companion :- default plus a replayed explicit-run guard case; it lost the batch cut to the four Critical fixes this round. Recorded as the first item for the next batch so it is not dropped.
延后到下一轮:把 SALVAGE_DIR 的创建/导入门控在 AUTO_REVIEW 上是针对显式运行的生产行为变更,还需配套的 :- 默认值与显式运行守卫回放用例;本轮让位于四个 Critical 修复。已记录为下一批次的首项,不会被遗漏。
| prompts: readFileSync(promptFile, 'utf8').split('\n').filter(Boolean), | ||
| timedOut, | ||
| }; |
There was a problem hiding this comment.
[Suggestion] R1-9: Still stands — the two salvage/supersede-cede early exits depend on exiting 0 (a non-zero cede exit would open the failure-fallback gate the cede exists to keep silent), but runScenario still swallows a non-zero exit — the catch restores e.stdout and the returned object carries no status — and both cede tests assert only transcript content. A mutant turning either cede path into a non-zero exit (fail() instead of exit 0, or a command failing before exit 0 under errexit) keeps every current assertion green — the transcripts still contain 'Superseded early'/'ceding' — while in production the non-zero exit opens the failure-fallback gate and posts the failure comment the cede exists to suppress. Exit 0 is load-bearing for both cede exits and nothing pins it.
Have runScenario return the exit status (status: e.status ?? 0 in the catch, 0 on the success path) and assert status === 0 in the supersede and salvage-armed cede scenarios.
Fix witness: the new status assertions — a mutant replacing either cede's exit 0 with exit 1 must turn them red.
中文说明
[Suggestion] R1-9:仍然存在——两条 salvage/取代让位的提前退出依赖以 0 退出(非零退出会打开让位本要保持沉默的失败兜底门),但 runScenario 仍吞掉非零退出——catch 恢复 e.stdout,返回对象不含退出码——两条让位测试也只断言转录内容。把任一条让位路径变成非零退出(用 fail() 替代 exit 0,或 errexit 下 exit 0 前有命令失败)的变异体保持现有全部断言绿色(转录仍含 'Superseded early'),而生产中非零退出会打开兜底门、发布让位本要压制的失败评论。exit 0 对两条让位都是承重项,却无任何钉住。
建议:runScenario 返回退出码(catch 中 status: e.status ?? 0,成功路径 0),两条让位场景断言 status === 0。
修复验证:把任一让位的 exit 0 换成 exit 1 后新断言必须变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
- Cede clean at the terminal fail when the live head really moved (a poll-gap move left no signal file) or when the PR timeline proves a move-then-revert landed during the run — a superseded run must not go red while its replacement is already queued. - Restart the one-shot watcher when a retry outlives it and clear the dead attempt's marker/moved-to/superseded signals, so the fresh attempt is watched and meets fresh state. - Measure salvage eligibility against the attempt's own budget share, not the whole run's, so a late retry can still reach KEEP. - New executed replays witness each guard; runScenario now returns the exit status so the load-bearing clean cede exits are pinned.
|
🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下: Round summary — PR #10123 (review-hold-and-salvage)Commit: No base merge was needed ( Critical findings — all four addressed
Suggestions addressed (batch capped at ~8 findings, Criticals first)
Deferred to the next round (recorded in
|
| Mutant | Witness that went red |
|---|---|
| pre-round tree (stash workflow) | all four Critical witnesses + signal-clear witness red pre-fix |
| pre-fail live-head cede removed | poll-gap replay |
| timeline-verified cede removed | revert replay (positive arm) |
| watcher relaunch removed | relaunch replay |
stale-signal rm removed |
signal-clear replay |
$attempt_budget → $BUDGET_SECONDS |
late-retry KEEP replay |
|| continue removed |
failing-gh poll replay |
[ -z "$live_head" ] conjunct removed |
empty-poll replay |
self-bound return 0 no-oped |
over-bound replay |
delay condition != → = |
verbatim pin |
cede_superseded exit 0 → exit 1 |
cede status assertions |
Verification
Commands actually run and their results:
npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-pr-review-workflow.test.js— 213 passed (206 pre-round + 7 new tests). Stability: 3 consecutive runs of the salvage block green (37/37 each).- Red-against-pre-round check (workflow stashed to the pre-round tree): the poll-gap, revert, relaunch, signal-clear, and own-budget witnesses all failed pre-fix, as the verification gate requires.
npx vitest run --config ./scripts/tests/vitest.config.ts(full scripts lane) — 1807 passed, 16 skipped, 2 failures, both outside this round's footprint and both verified not caused by this change:install-script.test.js > does not package audio-capture test artifactsreproduces identically on the pristine pre-round tree (pre-existing), andverify-capture.test.js > default-grey fallbackpasses in isolation both with and without this change (full-suite parallel-run contention).npm run typecheck— passed.npm run build— passed.npm run lint— passed.npx prettier --checkon both changed files — passed.bash .github/scripts/check-workflow-size.sh— passed (within the 4096-byte allowance of the recorded baseline; no baseline bump needed).- Workflow YAML re-parsed with the
yamllibrary;retryLoopSource()'s extraction window verified to contain all new loop-side code.
中文说明
本轮总结 — PR #10123(review-hold-and-salvage)
提交:ef4ec861ed fix(ci): cede genuinely superseded runs and restart the retry watcher
无需合并 base(--conflict false);无冲突。
Critical 发现 — 四项全部处理
- **R4-4(rc:3869272392)——轮询间隙内的取代被误报为红。**重试循环终局
fail之前,现在会重读 live head:若 head 确实移动则cede_superseded。落在观察者 ≤60 秒轮询间隙内的 head 移动不会留下任何信号文件,导致一次确实已被取代的 run 在接替 run 已排队的情况下以红色FAIL收场。见证:以移动 head 重放一次致命尝试(exit-3)→ 让位、退出码 0;未移动 head 的对照组保持FAIL。 - **R4-1(rc:3869272371)——让位杀树后 head 快速回退 / gh 抖动被误报为红。**新增
supersede_reverted_during_run:当OUTCOME != success、SUPERSEDE_FILE存在、但 live head 读回为已回退或不可读时,向 PR timeline 核验「移动后回退」的签名(GraphQLHEAD_REF_FORCE_PUSHED_EVENT事件在 run 开始(减 300 秒容差)之后落回期望 head)——timeline 事件是服务端数据,被评审 agent 无法伪造——吻合则让位;timeline 不可用、为空、过期或 head 不吻合则保留红色失败。见证:四臂回放(run 内吻合 → 让位退出 0;静默 / 过期 / 其他 head → 保持FAIL)。 - **R4-2(rc:3869272376)——重试带着陈旧信号且无人看守。**重试分支现在清除已死尝试的
moved-to/ salvage marker /superseded信号,并在一次性观察者已用完自身时重启它(WATCHER_PID非空且kill -0失败——bash 已回收退出的子 shell)。每个让位消费点都会复查 live head,因此清除后的状态是安全的。见证:回放内以armWatcher显式布防观察者(R3-1 的AUTO_REVIEW钉住保持不变):尝试 1 的观察者在短 head 上让位、head 回退、重试清除文件并重启观察者,尝试 2 早期的移动以 40 位十六进制 head 落下全新的 CEDE——缺少重启或缺少rm时让位消息中的 head 退化为unknown,断言变红。第二个回放直接钉住rm(尝试 2 观察到文件已被清除)。 - R4-3(rc:3869272386)——阈值把按尝试耗时与整 run 预算混用。
salvage_eligible现在接收该尝试自身的预算——BUDGET_SECONDS - (attempt_start - START_TS),即尝试开始时剩余的预算份额——因此剩余预算不足阈值的重试尝试仍能到达按耗时的 KEEP 分支,而不会在距发布几分钟时被 CEDE 杀掉。见证:重放观察者在 run 20000 秒 / 尝试 8000 秒下(现状 KEEP;旧分母下 CEDE,修复前为红),外加一个新尝试对照组仍让位。
已处理的 Suggestion(批次以约 8 项为上限,Critical 优先)
- R1-9(rc:3869272440)。
runScenario现在捕获并返回子进程退出码(e.status ?? 1;成功路径保持 0),六个让位 / 干净退出场景断言status === 0——非零让位(会打开失败兜底门)不再能绿色上线。探针:把cede_superseded的exit 0翻成exit 1后这些断言变红。 - R2-8(rc:3869272416)。
runWatcher新增failFirstPoll/emptyFirstPollgh stub 模式;观察者必须跳过失败 / 空轮询并仍然落下一次性的决策。探针:删除|| continue或删除[ -z "$live_head" ]合取项,各自使新用例变红。 - **R3-7(rc:3869272427)。**新增越界
runWatcher({ runElapsed: 23401 })用例,断言超过预算 + 30 分钟后观察者不做任何动作即返回。探针:把边界分支的return 0置空后变红。 - **R1-8(rc:3869272413)。**delay job 的陈旧 head 条件现在除 echo 字符串外还被逐字钉住(
[ "$current_head" != "$EVENT_HEAD_SHA" ])。探针:翻转操作符后该钉住变红。
延后到下一轮(已记录于 comment-replies.json,线程保持打开)
- R4-5(rc:3869272397)——守卫 marker 读取的 FIFO 放置见证(纯测试,resolve 套件)。
- R4-6(rc:3869272400)——
attempt-start重写的哨兵见证(纯测试)。 - R3-3 由修复引入(rc:3869272410)——harness 超时上限分类分支的正向 ETIMEDOUT 见证(纯测试)。
- R2-10(rc:3869272436)——把 SALVAGE_DIR 的创建 / 导入门控在
AUTO_REVIEW上(生产变更 +:-默认值 + 显式运行守卫回放);列为下一批次首项。
四项均因每轮批次上限让位于 Critical 修复;都不是拒绝处理。
变异探针
本提交新增的每个守卫都有执行式见证,并已在提交后的树上以探针验证(变异 → 定向 vitest → 变红 → 恢复):
| 变异体 | 变红的见证 |
|---|---|
| 轮前树(stash 掉工作流改动) | 四个 Critical 见证 + 信号清除见证在修复前全部为红 |
| 删除终局 fail 前的 live-head 让位 | 轮询间隙回放 |
| 删除经 timeline 验证的让位 | 回退回放(正向臂) |
| 删除观察者重启 | 重启回放 |
删除陈旧信号 rm |
信号清除回放 |
$attempt_budget → $BUDGET_SECONDS |
晚重试 KEEP 回放 |
删除 || continue |
gh 失败轮询回放 |
删除 [ -z "$live_head" ] 合取项 |
空轮询回放 |
自我边界 return 0 置空 |
越界回放 |
delay 条件 != → = |
逐字钉住 |
cede_superseded exit 0 → exit 1 |
让位退出码断言 |
验证
实际执行的命令及结果:
npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-pr-review-workflow.test.js—— 213 通过(轮前 206 + 新增 7 个测试)。稳定性:salvage 块连续 3 次运行全绿(每次 37/37)。- 对轮前树的变红检查(把工作流 stash 回轮前状态):轮询间隙、回退、重启、信号清除、自身预算五个见证在修复前均失败,符合验证门禁要求。
npx vitest run --config ./scripts/tests/vitest.config.ts(完整 scripts 泳道)——1807 通过、16 跳过、2 个失败,均在本轮足迹之外且已验证非本轮改动引入:install-script.test.js > does not package audio-capture test artifacts在轮前干净树上同样复现(预先存在);verify-capture.test.js > default-grey fallback在有无本轮改动的情况下单独运行均通过(完整套件并行运行的竞争所致)。npm run typecheck—— 通过。npm run build—— 通过。npm run lint—— 通过。npx prettier --check(两个改动文件)—— 通过。bash .github/scripts/check-workflow-size.sh—— 通过(在记录基线的 4096 字节容差内;无需上调基线)。- 工作流 YAML 已用
yaml库重新解析;已验证retryLoopSource()的提取窗口包含全部新增的循环侧代码。
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。
Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。
🧠 Handled by Qwen Code · model/模型 qwen3.8-max
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: reverse audit — stopped at the 5-round cap without converging (round 5 still reported findings).
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted; the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
Convergence: round 5 posted 15 inline comment(s), 11 of them reported for the first time; the previous round posted 12 (7 new). Findings keep coming back to the same files: .github/workflows/qwen-code-pr-review.yml (findings in rounds 2, 4; 8 more now); scripts/tests/qwen-pr-review-workflow.test.js (findings in rounds 1, 3; 2 more now). The rate of new findings is not falling. A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. Batching the remaining fixes and verifying them before the next push, or dropping this PR's reviews to --severity-floor critical, keeps the loop from re-deriving the same set. (Observation only — nothing was withheld from this review because of this observation.)
Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)
中文说明
仅完成部分审查,审查缺口已披露。
未审查:reverse audit — stopped at the 5-round cap without converging (round 5 still reported findings)。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted; the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
收敛情况:第 5 轮发布了 15 条行内评论,其中 11 条是首次提出;上一轮发布了 12 条(其中 7 条首次提出)。发现反复回到同一批文件:.github/workflows/qwen-code-pr-review.yml(第 2、4 轮已出过发现,本轮又有 8 条);scripts/tests/qwen-pr-review-workflow.test.js(第 1、3 轮已出过发现,本轮又有 2 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,或将本 PR 的评审降到 --severity-floor critical,可以避免循环反复推导同一组发现。(仅为观察——本轮评审未因此扣留任何内容。)
机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)
— qwen3.8-max via Qwen Code /review (v0.22.2)
| # dead attempt's signals — every cede consumer re-checks the | ||
| # live head, so the cleared state is safe. | ||
| rm -f "${SALVAGE_DIR}/moved-to" "${QWEN_CI_REVIEW_SALVAGE_OK_FILE:-}" "${SUPERSEDE_FILE:-}" | ||
| if [ -n "${WATCHER_PID:-}" ] && ! kill -0 "$WATCHER_PID" 2>/dev/null; then |
There was a problem hiding this comment.
[Critical] R4-2: (fix-induced) The watcher relaunch added this round to close R4-2 gates on a kill -0 liveness probe that races the one-shot watcher's own exit, so a retry attempt runs unwatched — and the PR's own test for exactly this behavior is red. When attempt 1's watcher cedes it is still draining its TERM→15s→KILL wind-down (or has exited but is unreaped — kill -0 succeeds on a zombie), so the probe sees it alive, the relaunch is skipped, and attempt 2 runs with no watcher: a head move during it is no longer killed within one poll interval and the run re-reviews the dead head for the whole remaining budget instead of ceding. Reproduced deterministically at this commit: relaunches a spent watcher and clears stale signals for a retry (replayed loop) fails 3/3, the suite runs 1 failed / 487 passed, and the required check Test (ubuntu-latest, Node 22.x) is red on this head.
Witness (test run at HEAD):
Test Files 1 failed | 3 passed
Tests 1 failed | 487 passed
✕ relaunches a spent watcher and clears stale signals for a retry (replayed loop)
expected '…to bbbb…b before the salvage threshold', received 'Superseded early: PR #1 moved from head-a to unknown…'
instrumented relaunch: DBG-RELAUNCH after pid=… alive=yes — no LAUNCH line
Stop gating on liveness; make the old watcher definitively dead and always start a fresh one before the new attempt:
if [ -n "${WATCHER_PID:-}" ]; then
kill "${WATCHER_PID}" 2>/dev/null || true
wait "${WATCHER_PID}" 2>/dev/null || true
fi
supersede_watcher &
WATCHER_PID=$!(wait reaps the zombie and, if the ceding watcher is still draining, bounds the block well inside the 60s backoff.) Fix witness: the failing test above must go green, and restoring the kill -0 probe must turn it red again.
中文说明
[Critical] R4-2:(由修复引入)本轮为关闭 R4-2 而新增的观察者重启逻辑以 kill -0 存活探测为闸门,与一次性观察者自身的退出过程竞态——重试尝试将在没有观察者的情况下运行,而本 PR 针对该行为的测试正是红的。尝试 1 的观察者让位后仍在收尾(TERM→15s→KILL),或已退出但未被回收(僵尸进程上 kill -0 仍成功),探测因此认为它存活、跳过重启,尝试 2 无观察者运行:其间的 head 移动不再被一个轮询间隔内杀掉,run 会用整个剩余预算重审死 head 而不是让位。在本提交上确定性复现:该测试 3/3 失败,套件 1 failed / 487 passed,必选检查 Test (ubuntu-latest, Node 22.x) 在本 head 上为红。
建议:不再以存活与否为闸门——先确保旧观察者确定死亡(kill + wait 回收)再无条件重启(上方代码)。修复验证:上述失败测试必须变绿;恢复 kill -0 探测后必须再次变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| while read -r oid at; do | ||
| [ "$oid" = "${EXPECTED_HEAD_SHA:-}" ] || continue | ||
| at="$(date -d "$at" +%s 2>/dev/null || echo 0)" | ||
| if [ "$at" -ge "$(( START_TS - 300 ))" ]; then |
There was a problem hiding this comment.
[Critical] R4-1: (fix-induced) The timeline revert-witness added this round to close R4-1 accepts any HEAD_REF_FORCE_PUSHED_EVENT landing back on the expected head within START_TS - 300 — including events from BEFORE the run started — so a forged SUPERSEDE_FILE can suppress an entire review with the job green and no replacement run queued. The synchronize trigger is shielded by the 10-minute environment wait, but the delay-less lifecycle triggers reopened/ready_for_review (both in the on: list, both skip the delay job) are attacker-controlled: force-push head H, close, reopen within ~4 minutes and START_TS lands inside the triggering push's timeline-event window. A prompt-injected reviewed agent (yolo, no sandbox) then plants $SALVAGE_DIR/superseded — derivable from the exported QWEN_CI_REVIEW_SALVAGE_OK_FILE — and steers the attempt into a non-retryable death; the head never moves, both live_head_moved checks return false, but the final look matches the triggering event → exit 0, green job, nothing posted, and no synchronize ever fired to queue a replacement.
Witness (probe over the PR's own replay harness):
STUB_TIMELINE='head-a <now>' → status 0, 'Superseded early:', 1 attempt (single recent back-push event = triggering-push shape)
controls (silent timeline / 2020-01-01 / other-head) → exit 1
Require the full move-then-revert pair inside the window, not a single back-push event: while reading the (ascending) events, record a move AWAY and return 0 only when a back-push to the expected head follows it within the window. Producing the witness then requires the head to have actually moved away during the run — precisely the condition that makes ceding correct and queues the replacement run.
Fix witness: the replay's ceded case must use an away/back pair, plus a new case asserting a timeline holding only the single recent back-push event keeps status 1 (FAIL); removing the pair requirement makes the new case red.
中文说明
[Critical] R4-1:(由修复引入)本轮为关闭 R4-1 而新增的 timeline 回退见证接受「任意落在 START_TS−300 窗口内、afterCommit 等于 expected head 的强推事件」——包括 run 开始之前就已存在的事件——因此伪造的 SUPERSEDE_FILE 可以让整次评审在 job 显示绿色、无接替 run 排队的情况下被无声压制。synchronize 触发因 10 分钟环境等待而受保护,但无延迟的生命周期触发 reopened/ready_for_review(均在 on: 列表、均跳过 delay job)完全由攻击者掌控:强推 head H、关闭、约 4 分钟内重新打开,START_TS 即落入触发 push 自己的 timeline 事件窗口。被提示注入的评审 agent(yolo、无沙箱)随后放置 $SALVAGE_DIR/superseded(路径可由导出的 QWEN_CI_REVIEW_SALVAGE_OK_FILE 推导)并让尝试以不可重试的方式死亡;head 从未移动、两处 live_head_moved 均为假,但终局检查匹配到触发事件 → exit 0、绿色、无任何发布、也没有 synchronize 曾触发排队接替。
建议:要求窗口内出现完整的「离开 + 返回」事件对,而不是单个回推事件(timelineItems 按时间升序返回,可在遍历时先记录离开事件,仅当其后出现回到 expected head 的回推时才返回 0)。这样伪造见证就必须让 head 在 run 期间真实离开过——而这恰是让位成立、接替 run 排队的前提。
修复验证:回放中 ceded 用例改用「离开/返回」事件对;新增用例断言「仅含单个近期回推事件」的 timeline 保持 status 1(FAIL);删除事件对要求后该用例变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| REPO: 'o/r', | ||
| }; | ||
| const now = new Date().toISOString(); | ||
| const ceded = runScenario('cede_revert_kill', { |
There was a problem hiding this comment.
[Critical] The cede_revert_kill replay executes the extracted loop's supersede_reverted_during_run(), whose timestamp parse is GNU-only date -d "$at" +%s — and the test_macos lane runs this suite in BSD userland where that parse silently degrades, so the test is red on every run of that lane. Lane chain: Test (macos-latest, Node 22.x) in ci.yml (gated to merge_group/schedule/workflow_dispatch) runs npm run test:ci → test:scripts; scripts/tests/vitest.config.ts excludes qwen-*-workflow.test.js only on win32, so macOS executes this file. BSD date rejects -d <timestamp> → at=0 → [ 0 -ge START_TS-300 ] is false → no cede → the loop falls to fail → exit 1, while the test asserts expect(ceded.status).toBe(0) — and a red merge-queue check ejects the entry and stalls the queue batch. The PR page shows green because the lane never runs for pull_request events. The sibling suite already proves the lane family lacks GNU date: qwen-fleet-shepherd-workflow.test.js defines a gnuDateShim (line 30) precisely to replay date -u -d … +%s portably; the new replay omits any shim.
Witness (probe flip through this repo's own harness, same input both arms):
GNU arm (Linux host): ✓ cedes a killed attempt whose superseding push reverted…
BSD arm (date shim exiting 1 on -d): ✕ AssertionError: expected 1 to be +0 (line 4663)
Make the replay portable the same way the fleet-shepherd suite does — prepend a date() shim that routes -d <date> +%s through node and passes everything else through — or probe the capability (spawnSync('date', ['-d', '@0', '+%s'])) and it.skipIf the GNU-only assertions where absent. The production workflow legitimately stays GNU-only (ubuntu jobs), so gate the test, not the script. Fix witness: the test must pass on a host whose date rejects -d after the fix; removing the shim/gate turns the lane red again by the mechanism above.
中文说明
[Critical] cede_revert_kill 回放会执行被提取循环中的 supersede_reverted_during_run(),其时间戳解析使用 GNU 专有的 date -d "$at" +%s;而 test_macos lane 在 BSD 用户态下运行本套件,该解析会静默降级,导致该测试在该 lane 上每次都红。链路:ci.yml 的 Test (macos-latest, Node 22.x)(merge_group/schedule/workflow_dispatch 触发)→ npm run test:ci → test:scripts;scripts/tests/vitest.config.ts 仅在 win32 排除 qwen-*-workflow.test.js,macOS 会执行本文件。BSD date 拒绝 -d <timestamp> → at=0 → 不让位 → 落入 fail → exit 1,与 expect(ceded.status).toBe(0) 相反;红色的合并队列检查会弹出条目并拖住队列批次。PR 页面显示绿色仅因 pull_request 事件不运行该 lane。同仓库的 qwen-fleet-shepherd-workflow.test.js 已用 gnuDateShim(30 行)处理同类问题;本回放没有任何 shim。
建议:按 fleet-shepherd 套件的既有方式加 date() shim(把 -d <date> +%s 交给 node,其余透传),或探测能力并对 GNU 专有断言使用 it.skipIf。生产 workflow 保持 GNU 专有是合理的(ubuntu job),应门控测试而非脚本。修复验证:修复后该测试必须在拒绝 -d 的宿主上通过;移除 shim/门控则按上述机制再次变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| # backoff too, and a head move landing in those 60s must meet | ||
| # fresh state (#10110). | ||
| reset_attempt_salvage_state() { | ||
| rm -f "${SALVAGE_DIR}/compose-seen" "${COMPOSED_ARTIFACT:-}" |
There was a problem hiding this comment.
[Critical] The salvage-state reset/cleanup removes agent-writable paths with bare rm -f under errexit — here and at the retry branch's rm -f "${SALVAGE_DIR}/moved-to" … (line 1828) — while every read site got a [ -f ] gate and every write got rename-over: a planted DIRECTORY aborts the step instead of being reset. The reviewed agent runs yolo/no-sandbox as the runner user and derives every signal path from its own exported environment (QWEN_CI_REVIEW_SALVAGE_OK_FILE is exported and exposes SALVAGE_DIR; the diff's own comment says so). During attempt 1 it can mkdir at compose-seen / moved-to / salvage-ok / superseded / COMPOSED_ARTIFACT. When the attempt ends retryably, rm -f on a directory exits 1 even with -f, and run_review_once restores set -e before returning — so the reset aborts the step: the job goes red with an rm diagnostic instead of retrying. Deleting SALVAGE_DIR outright fails one line later (mktemp in the gone dir → write_signal returns 1 → errexit abort). A directory planted at a write_signal TARGET is the silent variant: mv -f tmp dir exits 0 and moves the temp INTO the directory, so arming writes "succeed" while the signal never exists at its path. The file's own EXIT trap already meets this class with rm -rf.
Witness (probe, verbatim-extracted reset/retry code under set -euo pipefail):
compose-seen=dir → rm: cannot remove '…/compose-seen': Is a directory; reset_exit=1 (step aborts)
moved-to/salvage-ok/superseded=dirs → retry_rm_exit=1
salvage-ok=dir (silent) → WRITE_SIGNAL_EXIT_0, marker FILE ABSENT
FIXED (rm -rf + tolerant rewrite) → exit 0 in every case
Match the trap's convention at both cleanup sites — rm -rf (each operand is a fixed name inside the private mktemp dir or the exact artifact path, so -r cannot escape) and tolerate the rewrite failure (write_signal … || true; the watcher already falls back to START_TS on a bad attempt-start). Fix witness: extend the retry-replay harness (which already plants FIFOs via its plant: map) to plant a directory at compose-seen and/or COMPOSED_ARTIFACT during a retryable attempt-1 death and assert attempt 2 runs and the block exits 0; removing the rm -rf/tolerance makes it red.
中文说明
[Critical] salvage 状态的重置/清理用裸 rm -f 在 errexit 下删除 agent 可写路径——此处以及重试分支的 rm -f "${SALVAGE_DIR}/moved-to" …(1828 行)——而每个读站点都有 [ -f ] 闸门、每个写都是先写临时文件再 rename:放置一个目录即可让步骤中止而不是完成重置。被评审 agent 以 runner 用户 yolo/无沙箱运行,可从自身导出环境推导出所有信号路径(diff 自己的注释也这么说)。它可在尝试 1 期间对 compose-seen / moved-to / salvage-ok / superseded / COMPOSED_ARTIFACT 执行 mkdir。当尝试以可重试方式结束时:rm -f 作用于目录即使带 -f 也 exit 1,而 run_review_once 返回前已恢复 set -e——重置中止整个步骤:job 带着 rm 诊断信息变红而不是重试。直接删除 SALVAGE_DIR 会在下一行失败(在已消失的目录里 mktemp → write_signal 返回 1 → errexit 中止)。在 write_signal 目标上放置目录是静默变体:mv -f tmp dir exit 0 并把临时文件移入目录内部——布防写入「成功」但信号文件根本不存在于其路径。本文件自己的 EXIT trap 对同类路径已经使用 rm -rf。
建议:两处清理点都改用 rm -rf(每个操作数都是私有 mktemp 目录内的固定名称或确切的工件路径,-r 不会逃逸),并容忍重写失败(write_signal … || true;观察者在 attempt-start 损坏时本就回退到 START_TS)。修复验证:在重试回放装置(已有 FIFO 放置能力)中于可重试死亡期间放置目录,断言尝试 2 运行且代码块 exit 0;移除该修复后测试变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| run: |- | ||
| set -euo pipefail | ||
| BODY="$(printf '%s\n' \ | ||
| "<!-- qwen-review-salvaged ${EXPECTED_HEAD_SHA} -->" \ |
There was a problem hiding this comment.
[Critical] The salvaged-note marker qwen-review-salvaged is absent from qwen-autofix.yml's BOT_COMMENT_FILTER and every sibling bot-comment exclusion, so the autofix fleet scan counts the note itself — posted via CI_BOT_PAT as qwen-code-ci-bot, which is exactly REVIEW_BOT (qwen-autofix.yml:87; AUTOFIX_BOT is the different account qwen-code-dev-bot) — as new actionable review feedback. In the fleet scan, N_ISSUE_COMMENTS (~3249) keeps comments whose author is trusted or == REVIEW_BOT, then subtracts only the markers in BOT_COMMENT_FILTER (~3242), the ad-hoc <!-- qwen-review docs-only-medium exclusion, and @qwen-code / commands — this note passes all three. Concrete trigger: an autofix-engaged PR's automatic review salvages past threshold and posts its review plus this note. If the salvaged review was APPROVE (or its findings already addressed), N_REVIEWS is 0 (it counts only CHANGES_REQUESTED/COMMENTED) but N_ISSUE_COMMENTS ≥ 1 → the scan SELECTs the PR and dispatches a full autofix agent round whose only "feedback" is the informational note — the exact cost shape the workflow's own COMMAND_FILTER comment says it filters to avoid. When the salvaged review IS changes-requested, the note still double-counts and is loaded into the addressing agent's prompt (the same exclusion family recurs at ~4104/4192/4395/4427/4574). The docs-only-medium ad-hoc exclusion at exactly these sites is precedent that a new review-bot comment type requires a scan-side exclusion.
Witness (probe — the jq program extracted verbatim from qwen-autofix.yml:3249-3259, run with the workflow's actual constants):
salvaged note alone, real filter → N_ISSUE_COMMENTS = 1 (scan SELECTs the PR)
qwen-review-ack control → excluded (comparator not dead)
with qwen-review-salvaged in alternation → N_ISSUE_COMMENTS = 0
grep qwen-review-salvaged qwen-autofix.yml → no occurrences
Add qwen-review-salvaged to BOT_COMMENT_FILTER and the sibling exclusion regexes at the feedback-selection/watermark sites, and extend the filter pin at scripts/tests/qwen-autofix-workflow.test.js:17242 plus a cross-pin asserting the review workflow's note marker appears in the autofix filter (either side renaming then fails loudly). Fix witness: a scan-replay assertion that a lone salvaged note yields N_ISSUE_COMMENTS=0; removing the exclusion turns it red.
中文说明
[Critical] 本步骤发布的「历史 head 评审」说明携带标记 qwen-review-salvaged,但 qwen-autofix.yml 的 BOT_COMMENT_FILTER 与所有同族排除都没有包含它——该说明经 CI_BOT_PAT 以 qwen-code-ci-bot(即 REVIEW_BOT,qwen-autofix.yml:87;AUTOFIX_BOT 是另一个账号 qwen-code-dev-bot)发布,会被自治修复队列扫描计为新的可处理评审反馈。扫描的 N_ISSUE_COMMENTS(~3249)保留可信作者或 == REVIEW_BOT 的评论,仅减去 BOT_COMMENT_FILTER(~3242)、docs-only-medium 专门排除与 @qwen-code / 命令——本说明全部通过。触发:启用了自治修复的 PR 上,自动评审过阈值 salvage 并发布评审 + 本说明;若该评审是 APPROVE(或发现已被处理),N_REVIEWS 为 0 但 N_ISSUE_COMMENTS ≥ 1 → 扫描选中该 PR 并派发一整轮自治修复 agent,其唯一「反馈」就是这条信息性说明——正是 COMMAND_FILTER 注释声称要避免的开销形态;若评审是 changes-requested,说明还会被重复计数并注入处理 agent 的提示词(同族排除在 ~4104/4192/4395/4427/4574 反复出现)。docs-only-medium 的专门排除正是「新评审机器人评论类型需要扫描侧排除」的先例。
建议:把 qwen-review-salvaged 加入 BOT_COMMENT_FILTER 及各反馈选择/水位站点的同族排除正则;扩展 scripts/tests/qwen-autofix-workflow.test.js:17242 的过滤器钉住并加交叉钉住(任一侧改名即大声失败)。修复验证:扫描回放断言单独的 salvage 说明使 N_ISSUE_COMMENTS=0;移除排除后变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
There was a problem hiding this comment.
Deferred to the next round, not dropped. This round implemented eight findings under the per-round bound (Critical first: the failing-check watcher relaunch, the revert-witness forgery window, the hostile-state aborts, the trap order, the cede gating, the octal guard, the BSD-lane portability); this finding's fix lives in a separate file family (qwen-autofix.yml BOT_COMMENT_FILTER + six sibling exclusion sites + the filter pin/cross-pin) and is queued next with its scan-replay witness.
中文说明
延后至下一轮,不会丢弃。本轮按每轮批次上限实现了 8 条发现(Critical 优先:修复失败检查的观察者重启、回退见证伪造窗口、敌对状态中止、trap 顺序、让位闸门、八进制守卫、BSD 通道可移植性);本条的修复位于另一文件族(qwen-autofix.yml 的 BOT_COMMENT_FILTER + 六处同族排除站点 + 过滤器钉住/交叉钉住),已连同扫描回放见证一起排入下一轮。
| REPO: 'o/r', | ||
| REVIEW_URL: 'zz-no-such-review-url', | ||
| DOCS_ONLY_MEDIUM: 'false', | ||
| SALVAGE_ELAPSED_PERCENT: '50', |
There was a problem hiding this comment.
[Suggestion] The armWatcher env block arms a watcher whose KEEP decision calls salvage_eligible — defined OUTSIDE retryLoopSource()'s extraction window (workflow ~1386, before OUTCOME='' at ~1416) and never injected by the runScenario harness — so in every armWatcher replay the call fails 127 command not found inside the if condition (exempt from errexit, silent) and falls through to CEDE unconditionally: the KEEP branch is unreachable in this harness and the SALVAGE_ELAPSED_PERCENT env added here is dead in every replay. The below/above-threshold distinction these replays advertise is unpinned and unpinnable here — the passing relaunches a spent watcher test's comment stages a "below-threshold head move", but this harness has no threshold at all. The asymmetry is visible in-repo: the dedicated runWatcher harness DOES extract and inject the same function. Any future test author staging KEEP via armWatcher (e.g. a compose latch) will silently observe CEDE — a vacuous green, or a confusing red with no pointer to the missing function. Note the fix also owes QWEN_CI_REVIEW_SALVAGE_OK_FILE in extraEnv: with the function present, a KEEP verdict hits write_signal "$QWEN_CI_REVIEW_SALVAGE_OK_FILE" unbound under set -u.
Witness (probe, identical past-threshold input — 12000s into a 21600s budget, pct 50):
Arm A (runScenario shape, no injection): salvage_eligible: command not found → decision=CEDE
Arm B (runWatcher shape, function injected): decision=KEEP
KEEP arm under set -u without the marker var: bash: QWEN_CI_REVIEW_SALVAGE_OK_FILE: unbound variable
Inject the extracted salvage_eligible into the runScenario harness the way runWatcher does (prepend the regex-matched function before retryLoopSource() in the harness array), and supply QWEN_CI_REVIEW_SALVAGE_OK_FILE in extraEnv. Fix witness: a new armWatcher replay that latches compose-seen/COMPOSED_ARTIFACT before the head move and asserts moved-to + salvage-ok are written (KEEP); with the injection removed, the 127 forces CEDE and the assertion goes red.
中文说明
[Suggestion] armWatcher 环境变量块布防的观察者,其 KEEP 决策会调用 salvage_eligible——该函数定义在 retryLoopSource() 提取窗口之外(workflow ~1386,先于 ~1416 的 OUTCOME=''),runScenario 装置也从不注入它——因此在每一次 armWatcher 回放中,该调用都会在 if 条件内以 127 command not found 失败(errexit 豁免、静默)并无条件落入 CEDE:KEEP 分支在本装置中不可达,此处新增的 SALVAGE_ELAPSED_PERCENT 环境变量在所有回放中都是死的。这些回放宣称的「低于/高于阈值」区分在此无法钉住——通过的 relaunches a spent watcher 测试注释声称构造了「低于阈值的 head 移动」,但本装置根本没有阈值。不对称在仓库内可见:专门的 runWatcher 装置确实提取并注入了同一函数。未来任何试图用 armWatcher 构造 KEEP 的测试作者(例如 compose 锁存)都会静默观察到 CEDE——空洞的绿,或没有指向缺失函数的红。注意修复还需在 extraEnv 中提供 QWEN_CI_REVIEW_SALVAGE_OK_FILE:函数存在后,KEEP 判定会在 set -u 下展开未绑定的该变量。
建议:按 runWatcher 的方式把提取出的 salvage_eligible 注入 runScenario 装置(在装置数组中于 retryLoopSource() 之前前置该正则匹配到的函数),并在 extraEnv 提供 marker 变量。修复验证:新增 armWatcher 回放——head 移动前锁存 compose-seen/COMPOSED_ARTIFACT,断言写出 moved-to + salvage-ok(KEEP);移除注入后 127 强制 CEDE,断言变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
There was a problem hiding this comment.
Deferred to the next round, not dropped. Injecting salvage_eligible into the runScenario harness (the way runWatcher does) plus QWEN_CI_REVIEW_SALVAGE_OK_FILE in extraEnv and a KEEP-latch replay is queued behind the eight Criticals implemented this round. Note the salvage-armed cede replays added this round stage the marker via the stub qwen precisely because this injection is still missing — they document the dependency.
中文说明
延后至下一轮,不会丢弃。把 salvage_eligible 注入 runScenario 装置(按 runWatcher 的方式)、在 extraEnv 提供 QWEN_CI_REVIEW_SALVAGE_OK_FILE、新增 KEEP 锁存回放,排在本轮已实现的 8 条 Critical 之后。注意本轮新增的 salvage 布防让位回放正是由于该注入尚缺,才由桩 qwen 布防 marker——它们记录了这一依赖。
| # read: the path is agent-derivable, and a planted FIFO would | ||
| # block this cat — and with it the posting path — forever. | ||
| if [ -n "${QWEN_CI_REVIEW_SALVAGE_OK_FILE:-}" ] \ | ||
| && [ -f "${QWEN_CI_REVIEW_SALVAGE_OK_FILE}" ] \ |
There was a problem hiding this comment.
[Suggestion] R4-5: Still stands — this marker read is the one salvage-signal read with no planted-FIFO test, so its [ -f ] gate — which the comment above says keeps a planted FIFO from blocking the posting path "forever" — is unpinned. The marker path is agent-derivable (exported as QWEN_CI_REVIEW_SALVAGE_OK_FILE), as the diff's own comment states. If a future edit drops the [ -f ... ] && conjunct, every existing test stays green — the resolve-suite salvage tests only ever create regular files, and a FIFO behaves identically for the gate-free cat in those fixtures. A reviewed agent could then mkfifo the marker path; on a salvaged post (head moved past threshold) guard_pr_write's cat blocks forever on opening the FIFO, wedging the posting path until the step timeout — precisely the hang this gate's comment documents. Every sibling signal site got an explicit FIFO-plant test in this same diff (runWatcher plants superseded/moved-to/attempt-start/compose-seen FIFOs; runSalvageOutputs plants movedToFifo) — the guard the author added here is the one that can silently rot.
Extend runReviewGhWrapper in scripts/tests/qwen-resolve-workflow.test.js with a salvageFifo option (mkfifo instead of writeFileSync) and add a variant of lets a salvage-armed run post against its reviewed head after a move that plants the FIFO and asserts the wrapper decides within a bounded timeout — exit 90 ([ -f ] false → blocked), not a hang. Fix witness: with the [ -f ] gate removed, cat blocks on the FIFO open and the harness child hangs to its timeout instead of exiting 90 — the test goes red.
中文说明
[Suggestion] R4-5:仍然存在——本处 marker 读取是唯一一个没有「放置 FIFO」测试的 salvage 信号读取,因此其 [ -f ] 闸门(上方注释称它防止被放置的 FIFO 永久阻塞发布路径)没有被钉住。marker 路径可被 agent 推导(以 QWEN_CI_REVIEW_SALVAGE_OK_FILE 导出),diff 自己的注释也这么说。若未来编辑删除 [ -f ... ] && 子句,所有现有测试仍为绿——resolve 套件的 salvage 测试只创建普通文件,在这些夹具中不带闸门的 cat 对 FIFO 行为一致。被评审 agent 随即可对该路径 mkfifo;在一次过阈值 salvage 发布中,guard_pr_write 的 cat 会在打开 FIFO 时永久阻塞,把发布路径卡死到步骤超时——正是该闸门注释所描述的场景。同一 diff 中每个兄弟信号站点都有显式的 FIFO 放置测试(runWatcher 放置 superseded/moved-to/attempt-start/compose-seen FIFO;runSalvageOutputs 放置 movedToFifo)——唯独作者在此添加的守卫可能静默腐化。
建议:为 scripts/tests/qwen-resolve-workflow.test.js 的 runReviewGhWrapper 增加 salvageFifo 选项(以 mkfifo 代替 writeFileSync),并新增变体:放置 FIFO 后断言 wrapper 在有限超时内作出决定——exit 90([ -f ] 为假 → 阻止),而非挂起。修复验证:移除 [ -f ] 闸门后,cat 会阻塞在 FIFO 打开上直至超时,测试变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
There was a problem hiding this comment.
Deferred to the next round, not dropped. The planted-FIFO variant of the marker read in qwen-resolve-workflow.test.js (salvageFifo option + bounded-decision assertion) is queued behind the eight Criticals implemented this round.
中文说明
延后至下一轮,不会丢弃。qwen-resolve-workflow.test.js 中 marker 读取的「放置 FIFO」变体(salvageFifo 选项 + 有界决策断言)排在本轮已实现的 8 条 Critical 之后。
| # fresh state (#10110). | ||
| reset_attempt_salvage_state() { | ||
| rm -f "${SALVAGE_DIR}/compose-seen" "${COMPOSED_ARTIFACT:-}" | ||
| write_signal "${SALVAGE_DIR}/attempt-start" "$(date +%s)" |
There was a problem hiding this comment.
[Suggestion] R4-6: Still stands — the per-attempt-reset tests pin the compose-latch and composed-artifact removals (and the new clears the previous attempt's supersede signal test pins the retry-branch rm) but give no witness to this third component, the attempt-start rewrite — deleting this line ships the whole suite green. reset_attempt_salvage_state has three effects: remove compose-seen, remove COMPOSED_ARTIFACT, rewrite attempt-start; the compose_latch_reset / compose_artifact_reset / retry_clears_stale_signals scenarios pin the first two and the signal rm, nothing observes the rewrite. A mutant dropping it keeps attempt-start at run start, so a retried attempt's elapsed fraction counts from run start — exactly the staleness R4-3's per-attempt arithmetic exists to prevent — and every test stays green.
Add a replayed-loop witness: attempt 1 consumes a large fraction of the budget, dies retryably, and attempt 2's watcher (or an observation file the stub writes) sees attempt-start rewritten to ≥ the retry time — e.g. assert the second attempt's below-threshold move CEDEs with fresh-attempt arithmetic rather than KEEPing on run-level elapsed. Fix witness: removing the attempt-start rewrite line from the reset must turn that scenario red.
中文说明
[Suggestion] R4-6:仍然存在——按尝试重置的测试钉住了 compose 锁存与工件的删除(新的 clears the previous attempt's supersede signal 测试钉住了重试分支的 rm),却没有为第三个组件——attempt-start 重写——提供任何见证:删除这一行整个套件依然为绿。reset_attempt_salvage_state 有三个效果:删除 compose-seen、删除 COMPOSED_ARTIFACT、重写 attempt-start;compose_latch_reset / compose_artifact_reset / retry_clears_stale_signals 场景钉住了前两者与信号 rm,重写无人观察。删除它的突变体会让 attempt-start 停留在 run 开始时刻,于是重试尝试的已耗时比例从 run 开始起算——正是 R4-3 的按尝试算术要防止的陈旧性——而所有测试仍为绿。
建议:新增回放见证——尝试 1 消耗大部分预算后可重试死亡,尝试 2 的观察者(或 stub 写入的观察文件)看到 attempt-start 被重写为 ≥ 重试时刻;例如断言第二尝试低于阈值的移动按全新尝试算术 CEDE,而不是按 run 级耗时 KEEP。修复验证:从重置中移除 attempt-start 重写行必须使该场景变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
There was a problem hiding this comment.
Deferred to the next round, not dropped. The replayed-loop witness for the attempt-start rewrite (attempt 1 burns most of the budget, attempt 2 must CEDE on fresh-attempt arithmetic) is queued behind the eight Criticals implemented this round.
中文说明
延后至下一轮,不会丢弃。attempt-start 重写的回放见证(尝试 1 消耗大部分预算后,尝试 2 必须按全新尝试算术 CEDE)排在本轮已实现的 8 条 Critical 之后。
| }); | ||
| } catch (e) { | ||
| stdout = `${e.stdout ?? ''}`; | ||
| if (`${e?.error?.code ?? ''}` === 'ETIMEDOUT') { |
There was a problem hiding this comment.
[Suggestion] R3-3: Still stands — the round-3 fix for the harness hang added the 30s exec bound with this ETIMEDOUT classification branch, but the branch has no positive witness — the only assertion on timedOut is toBe(false) (line 4037) — and because the else branch restores fully buffered stdout, a broken check actively masks the exact regression the cap was added to surface. If the classification regresses (a wrong error-code shape, a Node version change), no test goes red — nothing else asserts the positive path — and the planted-FIFO hang returns silently to the CI lane. The shape is correct today (verified empirically on Node v22.23.2: both e.code and e.error.code carry 'ETIMEDOUT') but it is unpinned in the positive direction.
Add a positive witness: a scenario whose stub hangs deterministically (e.g. a FIFO plant the scenario opens for reading) asserting timedOut === true, so mutating the classification branch turns it red. Fix witness: removing or misspelling the ETIMEDOUT branch makes the new scenario red.
中文说明
[Suggestion] R3-3:仍然存在——第 3 轮为装置挂起问题增加的 30 秒执行上限带有这个 ETIMEDOUT 分类分支,但该分支没有正向见证——对 timedOut 的唯一断言是 toBe(false)(4037 行)——并且由于 else 分支恢复完整缓冲的 stdout,损坏的检查会主动掩盖该上限本要暴露的回归。若分类发生回归(错误的错误码形状、Node 版本变化),没有任何测试变红——再没有别处断言正向路径——放置 FIFO 的挂起会悄悄回到 CI lane。该形状目前是正确(已在 Node v22.23.2 实测:e.code 与 e.error.code 均为 'ETIMEDOUT'),但正向未被钉住。
建议:新增正向见证——一个确定性挂起的场景(例如场景打开一个 FIFO 读取),断言 timedOut === true;突变分类分支即变红。修复验证:移除或写错 ETIMEDOUT 分支会使新场景变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
There was a problem hiding this comment.
Deferred to the next round, not dropped. The positive timedOut === true witness for the ETIMEDOUT classification branch is queued behind the eight Criticals implemented this round.
中文说明
延后至下一轮,不会丢弃。ETIMEDOUT 分类分支的正向见证(timedOut === true)排在本轮已实现的 8 条 Critical 之后。
| # self-hosted runner, so a fixed path could be pre-seeded or left | ||
| # over. The watcher writes; guard_pr_write and the paths below read. | ||
| SALVAGE_DIR="$(mktemp -d "${RUNNER_TEMP:-/tmp}/qwen-review-salvage.XXXXXX")" | ||
| export QWEN_CI_REVIEW_SALVAGE_OK_FILE="${SALVAGE_DIR}/salvage-ok" |
There was a problem hiding this comment.
[Suggestion] R2-10: Still stands — the salvage escape applies to every run, but the marker it trusts is only ever legitimately armed by the supersede watcher, which is gated on AUTO_REVIEW=true; this setup/export is unconditional. In explicit runs (/review comments, review_requested, dispatch) no watcher ever arms, yet QWEN_CI_REVIEW_SALVAGE_OK_FILE — and thus SALVAGE_DIR — is exported into the reviewed agent's environment, where the escape can fire only via forgery: a prompt-injected agent writes the marker with the expected head (a value it also sees exported), and if the head later moves, guard_pr_write's salvage escape admits a historical-head post no watcher ever decided on. The marker family's accepted posture is documented ("a correctness gate, not a security boundary"), but the gating asymmetry remains: arming is AUTO_REVIEW-gated, exposure is not — and this fix has been deferred three rounds running.
Gate the SALVAGE_DIR setup/export on AUTO_REVIEW=true (explicit runs then never export the path), and default the post-loop read (${QWEN_CI_REVIEW_SALVAGE_OK_FILE:-}) so un-gated runs don't trip set -u; add a replayed explicit-run guard case. Fix witness: a guard case asserting an explicit run (AUTO_REVIEW=false) does not export QWEN_CI_REVIEW_SALVAGE_OK_FILE and the escape is unreachable; removing the gate makes it red.
中文说明
[Suggestion] R2-10:仍然存在——salvage 逃生口对所有 run 生效,但它信任的 marker 只会由以 AUTO_REVIEW=true 为门控的 supersede 观察者合法布防;而此处的建立/导出是无条件的。在显式运行(/review 评论、review_requested、dispatch)中,观察者从不布防,QWEN_CI_REVIEW_SALVAGE_OK_FILE(以及 SALVAGE_DIR)却仍被导出进被评审 agent 的环境——逃生口在这些 run 中只能经伪造触发:被提示注入的 agent 用其同样可见的 expected head 写下 marker,若 head 随后移动,guard_pr_write 的 salvage 逃生口会放行一次没有任何观察者决策过的历史 head 发布。marker 家族的既有立场已有文档(「正确性闸门,非安全边界」),但门控不对称仍在:布防有 AUTO_REVIEW 门控,暴露却没有——且该修复已连续三轮被延后。
建议:把 SALVAGE_DIR 的建立/导出以 AUTO_REVIEW=true 为门控(显式运行不再导出该路径),并为循环后的读取加 :- 默认值以免未门控运行触发 set -u;新增显式运行的守卫回放用例。修复验证:断言显式运行(AUTO_REVIEW=false)不导出 QWEN_CI_REVIEW_SALVAGE_OK_FILE 且逃生口不可达;移除门控后变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
There was a problem hiding this comment.
Deferred to the next round, not dropped. Gating the SALVAGE_DIR setup/export on AUTO_REVIEW=true (with the :- default on the post-loop read and an explicit-run guard replay) is queued behind the eight Criticals implemented this round. This round's R6-1 gate already shrinks the exposure on explicit runs at the cede sites themselves: the salvage-armed cede and both last-look cedes now refuse to fire without AUTO_REVIEW=true.
中文说明
延后至下一轮,不会丢弃。把 SALVAGE_DIR 的建立/导出以 AUTO_REVIEW=true 为门控(循环后读取加 :- 默认值、新增显式运行守卫回放)排在本轮已实现的 8 条 Critical 之后。本轮的 R6-1 闸门已在让位站点本身收窄了显式运行的暴露面:salvage 布防让位与两处终局让位现在没有 AUTO_REVIEW=true 即拒绝触发。
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- delay-job stale-head skip test: unpinned
headRefOidprojection — already reported as R1-8 (comment 3869272413, scripts/tests/qwen-pr-review-workflow.test.js:4998)
Not reviewed: reverse audit — stopped before round 4 by the review time budget.
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted; the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
Deferred under the convergence posture (round 6, not a blocker) — recorded, not requested in this round:
scripts/tests/qwen-pr-review-workflow.test.js:4818 — [review] D6-1 — note step EXPECTED_HEAD_SHA binding unpinnedscripts/tests/qwen-pr-review-workflow.test.js:4777 — [review] D6-2 — post-loop watcher reap pinned by no test.github/workflows/.size-baseline:39 — [review] R5-7 still stands — stale size baseline (183273 recorded vs 186929 actual).github/workflows/qwen-code-pr-review.yml:1801 — [review] R5-8 still stands — success-vs-supersede race posts a stale-head review unannotatedscripts/tests/qwen-pr-review-workflow.test.js:249 — [review] R5-9 still stands — armWatcher harness cannot exercise the watcher KEEP decision.github/workflows/qwen-code-pr-review.yml:1085 — [review] R4-5 still stands — marker-read [ -f ] gate unpinned by a planted-FIFO test.github/workflows/qwen-code-pr-review.yml:1616 — [review] R4-6 still stands — attempt-start rewrite has no witnessscripts/tests/qwen-pr-review-workflow.test.js:264 — [review] R3-3 still stands — ETIMEDOUT classification branch has no positive witness.github/workflows/qwen-code-pr-review.yml:1356 — [review] R2-10 still stands — salvage escape unconditional while the watcher is AUTO_REVIEW-gated
Convergence: round 6 posted 10 inline comment(s), 3 of them reported for the first time; the previous round posted 15 (11 new). Findings keep coming back to the same files: .github/workflows/qwen-code-pr-review.yml (findings in rounds 2, 4, 5; 3 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)
[Critical] R5-6 still stands — the trap-ordered rm -f "$LOG_PATH" poisonable-clause finding at .github/workflows/qwen-code-pr-review.yml:990 is re-verified at this commit (trap order unchanged) but is NOT re-posted inline: its round-5 thread already occupies that exact location (comment 3872716888 — the re-post exemption could not match it because that comment carries no id prefix and cross-references R1-9). The blocker remains open in that thread.
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:反向审计——评审时间预算不足,未能开始第 4 轮。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted; the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
收敛姿态下延后(第 6 轮,非阻断)——已记录,本轮不要求修改:共 9 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 6 轮发布了 10 条行内评论,其中 3 条是首次提出;上一轮发布了 15 条(其中 11 条首次提出)。发现反复回到同一批文件:.github/workflows/qwen-code-pr-review.yml(第 2、4、5 轮已出过发现,本轮又有 3 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)
[Critical] R5-6 still stands — the trap-ordered rm -f "$LOG_PATH" poisonable-clause finding at .github/workflows/qwen-code-pr-review.yml:990 is re-verified at this commit (trap order unchanged) but is NOT re-posted inline: its round-5 thread already occupies that exact location (comment 3872716888 — the re-post exemption could not match it because that comment carries no id prefix and cross-references R1-9). The blocker remains open in that thread.
— qwen3.8-max via Qwen Code /review (v0.22.2)
| # dead attempt's signals — every cede consumer re-checks the | ||
| # live head, so the cleared state is safe. | ||
| rm -f "${SALVAGE_DIR}/moved-to" "${QWEN_CI_REVIEW_SALVAGE_OK_FILE:-}" "${SUPERSEDE_FILE:-}" | ||
| if [ -n "${WATCHER_PID:-}" ] && ! kill -0 "$WATCHER_PID" 2>/dev/null; then |
There was a problem hiding this comment.
[Critical] R4-2: Still stands — the retry branch's watcher relaunch gates on a kill -0 "$WATCHER_PID" liveness probe that races the one-shot watcher's own exit. After a below-threshold cede the spent watcher is still draining (two pkills plus the 15s wind-down) when the retry branch checks it; kill -0 succeeds on the draining watcher, the relaunch is skipped, and the fresh attempt runs unwatched — a head move during it is never seen, the exact degradation this branch was added to prevent. This PR's own test for the guarantee is red at this commit: npm run test:scripts → Tests 1 failed | 1813 passed — relaunches a spent watcher and clears stale signals for a retry (replayed loop) fails 3/3 with Superseded early: … to unknown instead of a cede to the moved head (bash -x trace: watcher alive-but-unreaped at the check, no relaunch, attempt 2 dies at MAX_ATTEMPTS).
Witness:
npm run test:scripts → Test Files 1 failed | 65 passed (66); Tests 1 failed | 1813 passed
'relaunches a spent watcher…' red 3/3 — received 'Superseded early: … to unknown', expected 'to bbbb…'
Reap instead of probing: kill and wait the spent watcher, then relaunch unconditionally — any attempt reaching the retry branch has an exhausted one-shot watcher, and the 60s backoff absorbs the bounded drain:
| if [ -n "${WATCHER_PID:-}" ] && ! kill -0 "$WATCHER_PID" 2>/dev/null; then | |
| if [ -n "${WATCHER_PID:-}" ]; then | |
| kill "${WATCHER_PID}" 2>/dev/null || true | |
| wait "${WATCHER_PID}" 2>/dev/null || true | |
| supersede_watcher & | |
| WATCHER_PID=$! | |
| fi |
Fix witness: the replayed-loop test above is red today, must go green with the fix, and red again if the relaunch guarantee is removed.
中文说明
[Critical] R4-2:仍然存在——重试分支的观察者重启以 kill -0 "$WATCHER_PID" 存活探测为闸门,而该探测与一次性观察者自身的退出竞争。低于阈值的让位之后,耗尽的观察者仍在收尾(两次 pkill 加 15 秒缓冲)时重试分支就来检查,kill -0 对仍在收尾的观察者返回成功,重启被跳过,新尝试在无观察者状态下运行——期间的 head 移动永远不会被看到,正是该分支要防止的退化。本 PR 自己的对应测试在本提交上是红的:npm run test:scripts → Tests 1 failed | 1813 passed——relaunches a spent watcher and clears stale signals for a retry (replayed loop) 3/3 失败,输出 Superseded early: … to unknown 而非预期的向新 head 让位。建议改为回收而非探测:kill 并 wait 已耗尽的观察者后无条件重启(60 秒退避足以吸收这段有界收尾)。修复验证:上述回放测试当前为红,修复后必须变绿,移除重启保证后再次变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| while read -r oid at; do | ||
| [ "$oid" = "${EXPECTED_HEAD_SHA:-}" ] || continue | ||
| at="$(date -d "$at" +%s 2>/dev/null || echo 0)" | ||
| if [ "$at" -ge "$(( START_TS - 300 ))" ]; then |
There was a problem hiding this comment.
[Critical] R4-1: Still stands — supersede_reverted_during_run accepts any HEAD_REF_FORCE_PUSHED_EVENT landing back on the expected head within START_TS - 300 — including events from BEFORE the run started, contradicting the function's own rationale (a force-push that landed back 'during this run'). A force-push dated up to 300s before run start satisfies the window; the reviewed agent (yolo; SALVAGE_DIR is derivable from the exported QWEN_CI_REVIEW_SALVAGE_OK_FILE) plants ${SALVAGE_DIR}/superseded and forces a failed attempt; live_head_moved correctly refuses on the unmoved head, but the timeline branch then cedes anyway — exit 0, nothing posted, and no replacement run is queued because the head never moved: silent loss of review coverage for the current head. Explicit runs (/review, review_requested, dispatch) reach this branch with no gating at all.
Witness (probe driving the extracted retry loop):
event 200s BEFORE run start + forged file + unmoved head → status 0, "Superseded early" (green, nothing posted)
same input, event 400s before → status 1
window confined to the run's lifetime → status 1 (flips)
Make the witness discriminate: require the event to land during the run (at >= START_TS, seconds of skew tolerance), or additionally require a DEPARTURE event from the expected head during the run (beforeCommit.oid == EXPECTED_HEAD_SHA) — the triggering push can never satisfy it, a genuine move-then-revert always does.
Fix witness: a cede_revert_kill variant with the landing event inside the pre-run window must assert status 1 (no cede); it is red against the current window and red again if the fix is removed.
中文说明
[Critical] R4-1:仍然存在——supersede_reverted_during_run 接受任何落回预期 head 且时间戳在 START_TS - 300 以内的 HEAD_REF_FORCE_PUSHED_EVENT——包括 run 开始之前的事件,与函数自身注释('在本次 run 期间落回')矛盾。被评审 agent 可从导出的 QWEN_CI_REVIEW_SALVAGE_OK_FILE 推导出 SALVAGE_DIR,放置 superseded 文件并制造失败尝试后,live_head_moved 在 head 未移动时正确拒绝,但时间线分支仍会让位——exit 0、不发布任何评审,而 head 从未移动也不存在排队的接替 run:当前 head 的评审覆盖被静默丢失。显式运行(/review、review_requested、dispatch)完全无闸门地到达该分支。探针证据:运行开始前 200 秒的事件 + 伪造文件 + 未移动 head → status 0 绿色让位;窗口收紧到 run 生命周期内则翻转为 status 1。建议让见证具备判别力:要求事件落在 run 期间(at >= START_TS,容忍秒级时钟漂移),或额外要求 run 期间存在从预期 head 离开的事件(beforeCommit.oid == EXPECTED_HEAD_SHA)。修复验证:新增落点位于 run 前窗口内的变体断言 status 1。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| REPO: 'o/r', | ||
| }; | ||
| const now = new Date().toISOString(); | ||
| const ceded = runScenario('cede_revert_kill', { |
There was a problem hiding this comment.
[Critical] R5-1: Still stands — the cede_revert_kill replay executes the extracted loop's supersede_reverted_during_run, whose date -d "$at" +%s parse is GNU-only, but this suite also runs on the macOS lane (ci.yml test_macos, gated to merge_group/schedule/workflow_dispatch; scripts/tests/vitest.config.ts excludes win32 only). BSD date cannot parse an ISO timestamp, at falls back to 0, the START_TS - 300 window never passes, the run falls through to fail (status 1) instead of ceding, and expect(ceded.status).toBe(0) fails — red on every macOS-lane run. The lane reports skipped on the PR page, so green PR checks are no evidence; the red lands in the merge queue / nightly.
Witness (probe with a BSD-model date shim on the real test):
GNU date → 1 passed
BSD-model date → FAIL: AssertionError: expected 1 to be +0 (test.js:4682); silent/stale/other arms pass
The production step is legitimately single-platform (Linux runners) — the defect is the test asserting GNU behavior on a lane without it. Gate the replay on the capability using this file's own pattern (hasGnuMktemp/hasJq precedent — probe date -d <timestamp> +%s and it.skipIf(!hasGnuDate)), or inject a fleet-shepherd-style gnuDateShim into the harness so the coverage stays live on darwin.
中文说明
[Critical] R5-1:仍然存在——cede_revert_kill 回放执行提取循环中的 supersede_reverted_during_run,其 date -d "$at" +%s 解析仅 GNU 可用,但该套件同样运行在 macOS 通道(ci.yml 的 test_macos,由 merge_group/schedule/workflow_dispatch 触发;vitest 配置仅排除 win32)。BSD date 无法解析 ISO 时间戳,at 回退为 0,时间窗检查永不通过,运行落入 fail(status 1)而非让位,expect(ceded.status).toBe(0) 失败——macOS 通道每次运行都是红的。该通道在 PR 页面显示为 skipped,绿色的 PR 检查不构成证据。生产步骤本身是正当的单平台(Linux runner)——缺陷在于测试在没有 GNU date 的通道上断言 GNU 行为。建议按本文件既有模式做能力探测(hasGnuMktemp/hasJq 先例)并 it.skipIf,或像 fleet-shepherd 套件那样注入 gnuDateShim 保持 darwin 上的覆盖。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| # backoff too, and a head move landing in those 60s must meet | ||
| # fresh state (#10110). | ||
| reset_attempt_salvage_state() { | ||
| rm -f "${SALVAGE_DIR}/compose-seen" "${COMPOSED_ARTIFACT:-}" |
There was a problem hiding this comment.
[Critical] R5-2: Still stands — the salvage-state reset/cleanup removes agent-writable paths with bare rm -f under errexit — here and at the retry branch's signal cleanup (~:1828) — while every read site got a [ -f ] gate and every write got rename-over: a planted DIRECTORY aborts the step instead of being reset. The reviewed agent (yolo, same runner user) derives every signal path from its own exported environment and can mkdir at compose-seen / moved-to / salvage-ok / superseded / the composed artifact during attempt 1. rm -f on a directory exits 1 even with -f, and run_review_once restores set -e before returning — the reset aborts the step: the job goes red with an rm diagnostic instead of retrying. A directory planted at a write_signal target is the silent variant: mv -f tmp dir moves the temp INTO the directory — arming writes 'succeed' while the signal never exists at its path. The file's own EXIT trap already meets this class with rm -rf.
Witness (probe, verbatim-extracted reset/retry code under set -euo pipefail):
compose-seen=dir → rm: cannot remove '…/compose-seen': Is a directory; reset_exit=1
moved-to/salvage-ok/superseded=dirs → retry_rm_exit=1
salvage-ok=dir (silent) → write_signal exit 0, marker FILE ABSENT
rm -rf + tolerant rewrite → exit 0 in every case
Match the trap's convention at both cleanup sites — rm -rf (each operand is a fixed name inside the private mktemp dir or the exact artifact path, so -r cannot escape) and tolerate the rewrite failure (write_signal … || true).
Fix witness: extend the retry-replay harness (which already plants FIFOs via its plant: map) to plant a directory at compose-seen/COMPOSED_ARTIFACT during a retryable attempt-1 death and assert attempt 2 runs and the block exits 0; removing the fix makes it red.
中文说明
[Critical] R5-2:仍然存在——salvage 状态的重置/清理在 errexit 下用裸 rm -f 删除 agent 可写路径(此处以及重试分支 ~:1828 的信号清理),而每个读站点都有 [ -f ] 闸门、每个写都是临时文件+rename:放置一个目录即可让步骤中止而不是完成重置。被评审 agent 可从自身导出环境推导出所有信号路径,在尝试 1 期间对 compose-seen / moved-to / salvage-ok / superseded / compose 工件执行 mkdir。rm -f 作用于目录即使带 -f 也 exit 1,run_review_once 返回前已恢复 set -e——重置中止整个步骤:job 带着 rm 诊断变红而不是重试。在 write_signal 目标上放置目录是静默变体:mv -f tmp dir 把临时文件移入目录内部——布防写入'成功'但信号文件不存在。本文件自己的 EXIT trap 对同类路径已使用 rm -rf。建议两处清理点改用 rm -rf 并容忍重写失败。修复验证:重试回放装置中放置目录并断言尝试 2 运行且代码块 exit 0。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| run: |- | ||
| set -euo pipefail | ||
| BODY="$(printf '%s\n' \ | ||
| "<!-- qwen-review-salvaged ${EXPECTED_HEAD_SHA} -->" \ |
There was a problem hiding this comment.
[Critical] R5-3: Still stands — this step posts the note via CI_BOT_PAT as qwen-code-ci-bot, which is exactly REVIEW_BOT (qwen-autofix.yml:87), but the qwen-review-salvaged marker is absent from BOT_COMMENT_FILTER (:3242) and every sibling bot-comment exclusion — so the autofix fleet scan counts the note itself as new actionable review feedback. N_ISSUE_COMMENTS (:3249) keeps comments whose author is REVIEW_BOT and subtracts only the BOT_COMMENT_FILTER markers, the ad-hoc qwen-review docs-only-medium exclusion, and @qwen-code / commands — this note passes all three. Concrete shape: a salvaged APPROVE on an autofix-engaged PR leaves N_REVIEWS at 0 but N_ISSUE_COMMENTS ≥ 1 → the scan SELECTs the PR and dispatches a full autofix agent round whose only 'feedback' is the informational note; a changes-requested salvage double-counts the note into the addressing agent's prompt. The docs-only-medium note — same token family — needed exclusions at six sites (:3256/:4117/:4205/:4460/:4581/:4696); this note was added without its matching exclusion.
Witness:
grep qwen-review-salvaged .github/workflows/*.yml at this commit → one occurrence: this line
round-5 probe, the workflow's verbatim jq program with its real constants:
salvaged note alone → N_ISSUE_COMMENTS = 1 (scan SELECTs the PR)
marker added to the alternation → N_ISSUE_COMMENTS = 0
Add qwen-review-salvaged to BOT_COMMENT_FILTER and the sibling exclusion regexes at those six sites; extend the filter pin at scripts/tests/qwen-autofix-workflow.test.js:17242 plus a cross-pin asserting the review workflow's note marker appears in the autofix filter.
Fix witness: a scan-replay assertion that a lone salvaged note yields N_ISSUE_COMMENTS=0; removing the exclusion turns it red.
中文说明
[Critical] R5-3:仍然存在——本步骤经 CI_BOT_PAT 以 qwen-code-ci-bot(即 REVIEW_BOT,qwen-autofix.yml:87)发布说明,但 qwen-review-salvaged 标记不在 BOT_COMMENT_FILTER(:3242)及任何同族排除中——自治修复队列扫描会把该说明本身计为新的可处理评审反馈。salvage 后的 APPROVE 使 N_REVIEWS 为 0 而 N_ISSUE_COMMENTS ≥ 1 → 扫描选中该 PR 并派发一整轮 agent,其唯一'反馈'是这条信息性说明;changes-requested 时说明还会被重复注入处理 agent 的提示词。docs-only-medium 说明(同族标记)在六个站点都有排除,本说明却没有。建议把 qwen-review-salvaged 加入 BOT_COMMENT_FILTER 及 :3256/:4117/:4205/:4460/:4581/:4696 的同族排除,并扩展过滤器钉住与交叉钉住。修复验证:扫描回放断言单独的 salvage 说明使 N_ISSUE_COMMENTS=0。
— qwen3.8-max via Qwen Code /review (v0.22.2)
There was a problem hiding this comment.
Deferred to the next round (re-post of the round-5 finding above) — same disposition: the qwen-review-salvaged exclusion in BOT_COMMENT_FILTER and the six sibling sites are queued next round with the scan-replay witness (N_ISSUE_COMMENTS=0 for a lone salvaged note), behind the eight Criticals implemented this round.
中文说明
延后至下一轮(上方第 5 轮发现的重发)——处置相同:BOT_COMMENT_FILTER 及六处同族站点的 qwen-review-salvaged 排除,连同扫描回放见证(单独一条 salvage 说明使 N_ISSUE_COMMENTS=0)排入下一轮,位于本轮已实现的 8 条 Critical 之后。
| format('qwen-pr-review-pr-{0}', github.event.pull_request.number) || | ||
| format('qwen-pr-review-run-{0}', github.run_id) }} | ||
| cancel-in-progress: "${{ github.event_name == 'pull_request_target' && (github.event.action == 'synchronize' || github.event.action == 'closed') }}" | ||
| cancel-in-progress: "${{ github.event_name == 'pull_request_target' && github.event.action == 'closed' }}" |
There was a problem hiding this comment.
[Critical] R5-4: Still stands — dropping synchronize from cancel-in-progress introduced no cross-run dedup on the reviewed head, so an away-and-back force-push that fits inside the watcher's poll gap yields two full reviews and two review posts of the SAME commit. Run A reviews head H; a force-push to X and back to H lands within one SALVAGE_POLL_SECONDS=60 window (or every gh pr view poll fails during the X window via || continue, :1661) — no poll observes X: neither SUPERSEDE_FILE nor the salvage marker is written. A completes and posts its review of H. The away push's run sat PENDING in the PR concurrency group and was replaced by the back push's run C (pending-slot replacement, this file's own documented semantics, :60-63/:75); when A finishes, C starts, the delay job's stale-head skip compares current_head (H) against C's EVENT_HEAD_SHA (H) and proceeds — nothing anywhere dedups on 'head already reviewed'. C spends a full review budget re-reviewing H and posts a second review comment the fleet scan counts as fresh feedback. Pre-PR, the away push's cancel-in-progress killed A, so exactly one review posted.
witness: not run — requires GitHub Actions concurrency scheduling (pending-slot replacement, synchronize ordering) and a live force-push sequence on a real PR, none executable off-platform; every link above traced against the code at this commit.
Direction: head-level dedup before a run starts reviewing — in the delay re-check (next to the new EVENT_HEAD_SHA comparison), look up whether the bot already posted a review for the current head (posting markers would embed the reviewed head SHA the way the salvaged note does at :1932) and emit should_review=false when found.
Fix witness: a replay scenario where a successor run whose event head equals an already-posted head is skipped by the delay re-check; removing the dedup lookup must turn it red.
中文说明
[Critical] R5-4:仍然存在——把 synchronize 移出 cancel-in-progress 的同时没有为'已评审的 head'引入任何跨 run 去重:一次落在观察者轮询间隙(≤60s)内的'离开再回来'强推(或 X 窗口内每次 gh pr view 轮询都失败)会对同一个提交产生两次完整评审与两条评审发布。run A 评审 head H;强推到 X 再回到 H 未被任何轮询观察到——不写 SUPERSEDE_FILE 也不布防 marker;A 完成后发布对 H 的评审;离开 push 的 run 被回来 push 的 run C 顶替(待定位顶替是本文件自己文档化的语义);delay job 的过时 head 跳过比较 H 与 H 后放行——没有任何地方按'该 head 已被评审过'去重。C 花费完整预算重审 H 并发布第二条评审,被自治修复扫描计为新反馈。本 PR 之前,离开 push 的 cancel-in-progress 会杀掉 A,恰好只发布一条评审。见证:未运行——需要平台外的 GitHub Actions 并发调度与真实强推序列;上述每一环都已在本提交代码中逐行追踪。建议方向:在 delay 复查中按'机器人是否已对当前 head 发布过评审'去重,命中则 should_review=false。
— qwen3.8-max via Qwen Code /review (v0.22.2)
There was a problem hiding this comment.
Deferred to the next round (re-post of the round-5 finding above) — same disposition: the cross-run head-level dedup (delay re-check lookup against already-posted heads) is queued next round behind the eight Criticals implemented this round.
中文说明
延后至下一轮(上方第 5 轮发现的重发)——处置相同:跨 run 的 head 级去重(delay 复查对已发布 head 的查询)排入下一轮,位于本轮已实现的 8 条 Critical 之后。
| echo "qwen-review-salvage: head moved to ${live_head} ${elapsed}s in (compose_seen=${compose_seen}) — finishing and posting against reviewed head ${EXPECTED_HEAD_SHA} (#10110)" | ||
| return 0 | ||
| fi | ||
| write_signal "$SUPERSEDE_FILE" "$live_head" |
There was a problem hiding this comment.
[Critical] R5-5: Still stands — the watcher runs under inherited set -e (the step's set -euo pipefail propagates into backgrounded functions) and every write_signal call in it is an unguarded statement, so a failed signal write silently kills the watcher. write_signal returns 1 when mktemp "${SALVAGE_DIR}/.sig.XXXXXX" fails — reachable mid-run: SALVAGE_DIR is agent-derivable (this code's own comment says so) and a prompt-injected agent can rm -rf or chmod a-w it; a plain ENOSPC on the shared runner does the same. With SALVAGE_DIR gone, the first below-threshold head move kills the watcher HERE, before its pkill lines run: the attempt keeps reviewing the dead head for the rest of the budget while the queued replacement waits. A later retryable death reaches reset_attempt_salvage_state, whose own unguarded write_signal (~:1616) fails under the main step's errexit — the job aborts red instead of taking its one retry. This is the deletion dual of the planted-directory rm -f finding (R5-2).
Witness (probe, verbatim-extracted watcher, SALVAGE_DIR removed):
BASELINE: mktemp failure → watcher_exit=1, superseded=ABSENT, pkilled=NO
FIXED (|| true on decision writes): watcher_exit=0, cede message printed, pkilled=yes
reset half: BASELINE exits 1 before the retry proceeds; FIXED exit 0
Make the decision writes best-effort — write_signal … || true for superseded/moved-to/salvage-ok/compose-seen: every cede consumer re-authenticates against the live head, so a lost file degrades to the live re-check, not to silence. Guard the main-step reset rewrite too (|| true or mkdir -p before mktemp).
Fix witness: a watcher-replay sibling of cede_revert_kill with SALVAGE_DIR removed before the head move must assert the tree is still killed / the cede still taken and the retry loop survives the subsequent reset; removing the guards makes it red.
中文说明
[Critical] R5-5:仍然存在——观察者在继承的 set -e 下运行,其内每个 write_signal 都是无守卫语句——一次失败的信号写入会静默杀死观察者。SALVAGE_DIR 可被 agent 推导(代码注释自述),被提示注入的 agent 可 rm -rf 或 chmod a-w 之,共享 runner 上的 ENOSPC 同样如此。SALVAGE_DIR 消失后,第一次低于阈值的 head 移动会在 pkill 行执行之前杀死观察者:尝试继续用整个剩余预算重审死 head,排队的接替 run 干等;之后可重试死亡到达 reset_attempt_salvage_state,其自身无守卫的 write_signal 在主步骤 errexit 下失败——job 中止变红而不是执行唯一的重试。这是 R5-2(放置目录 + rm -f)的删除侧对偶。探针证据:移除 SALVAGE_DIR 后观察者 exit 1、无信号、无 pkill;决策写入加 || true 后让位照常。建议决策写入尽力而为,并守卫主步骤的重置重写。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| if live_head_moved; then | ||
| cede_superseded | ||
| fi | ||
| if [ -f "${SUPERSEDE_FILE:-}" ] && supersede_reverted_during_run; then |
There was a problem hiding this comment.
[Critical] R6-1: The 'last look' cede is gated on live-head state alone and NOT on AUTO_REVIEW — it fires on explicit runs (/review comment, review_requested, workflow_dispatch) where no watcher runs and no replacement run is queued: any failed review concurrent with a head move exits 0 green claiming it 'cedes to the replacement run' — which does not exist. Concrete shape: a maintainer comments @qwen-code /review on a draft PR — the issue_comment branch of review-pr.if (:418-427) checks only issue.state == 'open' while the lifecycle branch requires !draft (:415), so no synchronize-triggered replacement exists; the author pushes mid-review, the attempt fails terminally, and the ungated last look cedes — no review posted, and no fallback comment either (the gate keys on result == 'failure', :2452). Same silent loss on docs-only PRs: the replacement lifecycle run downgrades to the medium relay that never posts. This contradicts the diff's own design — the watcher block states 'Explicit runs (/review, review_requested, dispatch) keep today's behavior' — and the PR's own test pins that rationale (:4016-4018). The three sibling cede sites are implicitly auto-only because only the AUTO-gated watcher writes their signal files; this site is the one reachable on explicit runs without any forged file.
Witness (probe replaying the real extracted loop):
AUTO_REVIEW=false, expected=head-a, live=head-b → exit 0, "Superseded early: … moved from head-a to unknown … ceding to the replacement run"
AUTO_REVIEW=false, live=head-a (control) → exit 1, "FAIL kind=[] reason=[Qwen review exited with status 3.]"
same as arm 1, last look gated on AUTO_REVIEW → exit 1 (flips)
Gate the last look on the watcher-arming condition — if [ "${AUTO_REVIEW:-false}" = "true" ] && live_head_moved; then cede_superseded; fi — and defense-in-depth the same gate on the salvage-armed cede, whose marker path is exported and forgeable on an explicit run when a real head move coincides.
Fix witness: a replay scenario with armWatcher: false, a failing outcome, and the gh stub reporting a moved head must end at the harness's FAIL … line (non-zero status), never at 'Superseded early'; removing the gate turns it red.
中文说明
[Critical] R6-1:'最后看一眼'让位只以 live head 状态为闸门,未加 AUTO_REVIEW 闸门——它会在显式运行(/review 评论、review_requested、workflow_dispatch)上触发,而这些运行既无观察者也没有排队的接替 run:任何与 head 移动并发的失败评审都会以 exit 0 变绿,声称'让位给接替 run'——而该 run 并不存在。具体形态:维护者在草稿 PR 上评论 @qwen-code /review——review-pr.if 的 issue_comment 分支只检查 issue.state == 'open',而生命周期分支要求 !draft——因此不存在 synchronize 触发的接替;作者评审中途 push、尝试以终结性方式失败,无闸门的最后看一眼让位——评审没有发布,兜底评论也不会发布(其闸门以 result == 'failure' 为键)。docs-only PR 上同样静默丢失:接替的生命周期 run 降级为从不发布的 medium 中继。这与 diff 自己的设计矛盾——观察者代码块写明'显式运行保持现状',且本 PR 自己的测试钉住了该理由。探针证据:AUTO_REVIEW=false + head 已移动 → exit 0 'Superseded early… to unknown';head 未移动对照臂 → exit 1;加上 AUTO_REVIEW 闸门后翻转为 exit 1。建议给最后看一眼加上与观察者布防相同的闸门,并纵深防御地同样守卫 salvage 布防让位。修复验证:armWatcher: false + 失败结果 + head 已移动的回放必须以非零 FAIL 结束,绝不能出现 'Superseded early'。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| attempt_start="$(cat "${SALVAGE_DIR}/attempt-start" 2>/dev/null || true)" | ||
| fi | ||
| case "$attempt_start" in | ||
| ''|*[!0-9]*) attempt_start="$START_TS" ;; |
There was a problem hiding this comment.
[Critical] R6-2: The digit-only case guard on the agent-writable attempt-start signal misses the octal class. SALVAGE_DIR is derivable from the exported QWEN_CI_REVIEW_SALVAGE_OK_FILE (the diff's own comment states the derivation surface), so the reviewed agent can write 08 into attempt-start; the ''|*[!0-9]* pattern accepts it (all digits); on the next poll with a moved head, elapsed=$(( $(date +%s) - attempt_start )) hits 'value too great for base' — the poll loop aborts with NO decision written and no pkill, and nothing detects the dead watcher (relaunch happens only in the retry branch): the run reviews the dead head for the rest of the budget while the queued replacement waits — the #9729 failure this PR exists to fix. A zero-padded octal-valid value (e.g. 01753000000 → 262930432) evaluates in base 8 and forces KEEP from the first poll — that arm stays inside the documented agent-forgery residual (the agent can force KEEP more simply via compose-seen), but the watcher-death arm is not covered by any documented residual. The same file one screen above force-decimalizes SALVAGE_ELAPSED_PERCENT with 10# under a comment naming exactly this hazard ('08 kills the watcher's first salvage arithmetic').
Witness (probe driving the real extracted watcher):
attempt-start=08, moved head → "08: value too great for base" — superseded=null, salvage-ok=null, pkill fired=false
decimal control → cede printed, superseded=head-b, pkill fired=true
$((01753000000)) → 262930432 → salvage_eligible true from the first poll
Reject leading zeros in the guard — case "$attempt_start" in ''|0*|*[!0-9]*) attempt_start="$START_TS" ;; esac (epoch seconds never start with 0 before 2286). Do not use a bare 10# coercion — 10#08 = 8 yields elapsed ≈ now, converting the crash into forced KEEP; a START_TS ≤ value ≤ now range check is the fully robust variant.
Fix witness: a watcher-replay case planting 08 into attempt-start with a moved head must assert the watcher still acts (writes SUPERSEDE_FILE / arms the marker); removing the 0* rejection leaves the watcher dead and the assertion red.
中文说明
[Critical] R6-2:对 agent 可写的 attempt-start 信号,仅数字的 case 闸门漏掉了八进制类。SALVAGE_DIR 可由导出的 QWEN_CI_REVIEW_SALVAGE_OK_FILE 推导(diff 自己的注释说明了该推导面),被评审 agent 可写入 08;''|*[!0-9]* 模式接受它;下一次 head 已移动的轮询中 elapsed=$(( $(date +%s) - attempt_start )) 触发 'value too great for base'——轮询循环中止,没有写下任何决策、没有 pkill,且没有任何机制检测观察者已死(重启只发生在重试分支):run 用剩余整个预算重审死 head,排队的接替 run 干等——正是本 PR 要修复的 #9729 失败。零填充的合法八进制值(如 01753000000 → 262930432)按 8 进制求值,从第一次轮询起强制 KEEP——该臂在文档化的伪造残余之内(agent 可通过 compose-seen 更简单地强制 KEEP),但观察者死亡臂没有任何文档化残余覆盖。同文件一屏之上对 SALVAGE_ELAPSED_PERCENT 用 10# 强制十进制,注释明确点名同一危害。探针证据:放置 08 后观察者静默死亡、无信号、无 pkill;十进制对照正常让位。建议在闸门中拒绝前导零(''|0*|*[!0-9]*,epoch 秒在 2286 年前不会以 0 开头);裸 10# 强转会把崩溃转成强制 KEEP,START_TS ≤ 值 ≤ now 的范围检查才是完全稳健的变体。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| # next poll instead. Residual risk, documented like the marker | ||
| # escape: an agent can write valid JSON too, but it already | ||
| # authors the review this signal describes. | ||
| if [ ! -f "${SALVAGE_DIR}/compose-seen" ] && [ -f "$COMPOSED_ARTIFACT" ] \ |
There was a problem hiding this comment.
[Critical] R6-3: The watcher's compose-latch FIFO defense is a check-then-open TOCTOU: [ -f ] passes on a regular file, then node -e …readFileSync REOPENS the path — a swap to a FIFO inside that window blocks the watcher permanently. The artifact path is agent-derivable (cwd + PR number — the diff's own comment acknowledges agent control). A prompt-injected agent rename-swaps it between a valid-JSON regular file and a FIFO: on every poll where [ -f ] sees the regular phase node forks, and a swap landing inside the measured 17-20ms startup-to-open window (against a µs rename) makes open(O_RDONLY) hit the FIFO — blocked forever. Consequences, all traced at this commit: CEDE never fires (a superseded run burns its whole remaining budget on the stale head while the replacement waits in the queue); KEEP is never armed (a near-complete run dies unposted — the #9729 loss this feature exists to prevent); and the retry relaunch's kill -0 reads the hung watcher as alive, so attempt 2 runs unwatched. The same race reaches read_head_signal's cat (:1735 — hangs the finished step's exit path) and the attempt-start cat (:1674). The suite's FIFO test (:4261+) plants the FIFO BEFORE the poll — it pins only the static case [ -f ] rejects.
Witness (probe driving the production watcher against a rename-swap loop):
as-PR arm → HUNG: no decision after 15s; blocked node on composed.json visible in ps (1/3 trials)
timeout-5 reopen → DECIDED 3/3 (CEDE / KEEP / CEDE)
read_head_signal under the swapper → 15/30 hangs
node startup window → 17-20 ms measured, vs a µs rename swap
Bound every watcher reopen so a blocking open cannot outlive the poll: timeout 5 node -e … for the latch (a timed-out miss retries next poll), and the same bound on the attempt-start and read_head_signal cats — each degrades into a fallback that already exists.
Fix witness: extend the FIFO suite with a race variant cycling composed.json between a regular file and a FIFO while the watcher runs, asserting it still reaches a decision within the harness bound; removing the timeout bound wedges the latch and the assertion goes red.
中文说明
[Critical] R6-3:观察者的 compose 锁存 FIFO 防御是检查后打开的 TOCTOU:[ -f ] 在常规文件上通过,随后 node -e …readFileSync 重新打开该路径——在此窗口内换成 FIFO 会永久阻塞观察者。工件路径可由 agent 推导(cwd + PR 编号——diff 自己的注释承认 agent 可控)。被提示注入的 agent 以 rename 在'合法 JSON 常规文件'与 FIFO 之间轮换:每当 [ -f ] 看到常规相,node 启动,而落在实测 17-20ms 启动窗口内的一次轮换(相对 µs 级的 rename)使 open(O_RDONLY) 命中 FIFO——永久阻塞。后果(均在本提交代码中追踪):CEDE 永不触发(被取代的 run 烧完整个剩余预算,接替 run 在队列里干等);KEEP 永不布防(接近完成的 run 死亡而不发布——正是本特性要防止的 #9729 损失);重试重启的 kill -0 把挂起的观察者读作存活,尝试 2 无观察者运行。同一竞态还波及 read_head_signal 的 cat(:1735,挂起已完成步骤的退出路径)与 attempt-start 的 cat(:1674)。套件的 FIFO 测试在轮询之前放置 FIFO,只钉住了 [ -f ] 拒绝的静态情形。探针对生产观察者驱动 rename 轮换循环:现状代码 → 挂起(15 秒无决策,ps 可见被阻塞的 node);为重新打开加 timeout 5 → 3/3 正常决策。建议给观察者的每次重新打开加上界(锁存用 timeout 5 node -e …,attempt-start 与 read_head_signal 的 cat 同理),超时未命中退回既有回退路径。修复验证:FIFO 套件增加轮换竞态变体,断言观察者仍能在装置上界内做出决策。
— qwen3.8-max via Qwen Code /review (v0.22.2)
There was a problem hiding this comment.
Deferred to the next round, not dropped. The timeout-bounded reopens (compose-latch node read, attempt-start and read_head_signal cats) plus the rename-swap race witness are queued next round behind the eight findings implemented this round. Note this round's R4-2 fix removes one of the finding's consequences already: the retry relaunch no longer reads a hung watcher as alive (it kills and waits instead of probing).
中文说明
延后至下一轮,不会丢弃。timeout 有界的重新打开(compose 锁存的 node 读取、attempt-start 与 read_head_signal 的 cat)连同 rename 轮换竞态见证排入下一轮,位于本轮已实现的 8 条发现之后。注意本轮的 R4-2 修复已消除本条的一个后果:重试重启不再把挂起的观察者读作存活(改为 kill + wait,而非探测)。
|
🤖 Addressed the latest review feedback (round 2/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 2/100 轮)。改动内容与我反驳保留之处如下: Round summary — PR #10123Merged Conflict resolution (origin/main merge)
Findings implemented
Findings deferred to the next round (round bound: ~8, Critical first)
All deferred findings keep their threads open; each gets a reply on its own thread via Verification
中文说明本轮总结 — PR #10123先合并 冲突解决(origin/main 合并)
已实现的发现
延后至下一轮的发现(批次上限约 8 条、Critical 优先)
所有延后发现的线程保持打开,并通过 验证
Base-conflict check · 基分支冲突检查: conflicted with main — resolved in this push. · 与 main 有冲突——已在本次推送中解决。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
7 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- report-step hold fail-open probe fallbacks untested — already reported in the round-4 deferral list ('hold tests pin jq filters but not the ROLLUP_R/LIVE_HEAD_R fetches', scripts/tests/qwen-autofix-workflow.test.js:21757, review 5037862190)
- qwen-autofix.md af-028 stale synchronize-cancel wording — already reported in the round-4 deferral list ('af-028 rationale describes the removed synchronize-cancel', .github/workflows/qwen-autofix.md:1102, review 5037862190)
- guard_pr_write salvage escape ungated for explicit runs / SALVAGE_DIR export unconditional — already reported as R2-10 (comment 3865698458, deferred in round 6)
- deferred-headline stale cancel wording — already reported as comment 3862832385 (round 1, accepted and deferred)
- runScenario/armWatcher harness cannot exercise the watcher KEEP decision — already reported as R5-9 (round 5, recorded deferred in the round-6 body)
- delay-job stale-head skip test: unpinned headRefOid fetch/read wiring — already reported as R1-8 (comment 3869272413)
- wrapper salvage marker-read [ -f ] gate unpinned by a planted-FIFO test — already reported as R4-5 (comments 3869272397 / 3872716975, deferred in round 6)
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted; the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
Deferred under the convergence posture (round 7, not a blocker) — recorded, not requested in this round:
.github/workflows/qwen-code-pr-review.yml:1922 — [review] D7-1 — post-loop salvaged outputs block misses the AUTO_REVIEW gatescripts/tests/qwen-autofix-workflow.test.js:23129 — [review] D7-2 — design-doc test pins af-149 instead of af-151scripts/tests/qwen-resolve-workflow.test.js:215 — [review] D7-4 — nothing pins the synchronize trigger type.github/workflows/qwen-autofix.yml:5982 — [review] D7-5 — report-time hold is blind to watcher-less command runs.github/workflows/qwen-code-pr-review.yml:1633 — [review] D7-6 — salvage reset/trap rm -rf calls lack || true.github/workflows/qwen-code-pr-review.yml:2595 — [review] D7-7 — fallback-gate rationale keeps a stale cancel claim
Convergence: round 7 posted 5 inline comment(s), 2 of them reported for the first time; the previous round posted 10 (3 new). Findings keep coming back to the same files: .github/workflows/qwen-code-pr-review.yml (findings in rounds 4, 5, 6; 2 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)
中文说明
本轮确认的 7 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted; the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
收敛姿态下延后(第 7 轮,非阻断)——已记录,本轮不要求修改:共 6 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 7 轮发布了 5 条行内评论,其中 2 条是首次提出;上一轮发布了 10 条(其中 3 条首次提出)。发现反复回到同一批文件:.github/workflows/qwen-code-pr-review.yml(第 4、5、6 轮已出过发现,本轮又有 2 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)
— qwen3.8-max via Qwen Code /review (v0.22.2)
| if [ -f "${SUPERSEDE_FILE:-}" ] && live_head_moved; then | ||
| cede_superseded | ||
| fi |
There was a problem hiding this comment.
[Critical] R7-1: The two SUPERSEDE_FILE cede checks (this top-of-loop site and its post-attempt twin at ~:1840) are not gated on AUTO_REVIEW, while their sibling cede sites — the salvage-armed cede (:1841) and both last-look cedes (:1897, :1900) — received the gate in the R6-1 fix. An explicit run (/review comment, review_requested, workflow_dispatch) has no watcher and no queued replacement, so SUPERSEDE_FILE can only exist there by forgery — and the path is derivable by the reviewed agent from the exported QWEN_CI_REVIEW_SALVAGE_OK_FILE (its dirname, superseded sibling). The live_head_moved re-check cannot defend this shape because the PR author controls the head: an explicit run whose attempt ends non-success (any transient API error) after an author push exits 0 green through cede_superseded, posting nothing, with no replacement run and the failure-fallback gate closed on the clean exit — the maintainer-requested review is silently suppressed.
Witness (probe over the extracted loop at HEAD): AUTO_REVIEW=false, forged marker, moved head, retryable death → exit_status=0, "Superseded early: … ceding to the replacement run (#10110)." — green, nothing posted. With the gate applied to both sites: exit_status=1 → FAIL reason=[Qwen review aborted with an API error before posting comments.] — loud red. Control (AUTO_REVIEW=true, gated): exit_status=0, Superseded early: — the intended lifecycle path is preserved.
| if [ -f "${SUPERSEDE_FILE:-}" ] && live_head_moved; then | |
| cede_superseded | |
| fi | |
| if [ "${AUTO_REVIEW:-false}" = "true" ] && [ -f "${SUPERSEDE_FILE:-}" ] && live_head_moved; then | |
| cede_superseded | |
| fi |
Apply the same conjunct to the post-attempt check at ~:1840 (if [ "$OUTCOME" != "success" ] && [ "${AUTO_REVIEW:-false}" = "true" ] && [ -f "${SUPERSEDE_FILE:-}" ] && live_head_moved). Note the existing replay ends a superseded attempt clean without retrying runs this exact shape with AUTO_REVIEW=false and asserts the green cede — it must move to armWatcher with the fix.
Fix witness: a replay with AUTO_REVIEW=false, forged SUPERSEDE_FILE, moved head and a retryable death must assert attempts=2, status 1 and a FAIL line; removing the gate turns it back to a green cede.
中文说明
[Critical] R7-1:两处 SUPERSEDE_FILE 让位检查(本处循环顶部站点,以及 ~:1840 的尝试后孪生站点)没有 AUTO_REVIEW 闸门,而其兄弟让位站点——salvage 布防让位(:1841)与两处 last-look 让位(:1897、:1900)——在 R6-1 修复中都已加上该闸门。显式运行(/review 评论、review_requested、workflow_dispatch)既无 watcher 也无排队的接替 run,因此 SUPERSEDE_FILE 在那里只可能来自伪造——且该路径可被评审 agent 从导出的 QWEN_CI_REVIEW_SALVAGE_OK_FILE(其 dirname 的 superseded 兄弟文件)推导出来。live_head_moved 复查无法防御这一形态,因为 PR 作者控制着 head:显式运行的尝试在作者 push 之后以非成功结束(任何瞬时 API 错误)时,会经由 cede_superseded 以 0 绿色退出——不发布任何内容、没有接替 run、失败兜底门因干净退出而保持关闭——维护者请求的评审被无声压制。
验证证据(对 HEAD 提取的 loop 做探针):AUTO_REVIEW=false、伪造 marker、head 移动、可重试死亡 → exit_status=0、"Superseded early: … ceding to the replacement run" —— 绿色、无任何发布;对两个站点加上闸门后 → exit_status=1 → FAIL —— 红色显性失败;对照(AUTO_REVIEW=true、有闸门)→ exit_status=0, Superseded early: —— 生命周期预期路径不受影响。
建议修复:在两处检查中加入 [ "${AUTO_REVIEW:-false}" = "true" ] && 合取项。注意现有回放用例 ends a superseded attempt clean without retrying 正是以 AUTO_REVIEW=false 运行该形态并断言绿色让位——修复时需将其改为 armWatcher。
修复验证:新增 AUTO_REVIEW=false、伪造 SUPERSEDE_FILE、head 移动且可重试死亡的回放,断言 attempts=2、状态 1 与 FAIL 输出;移除闸门后该测试回退为绿色让位(变红)。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| if [ "$departed" = "1" ] && [ "$after" = "${EXPECTED_HEAD_SHA:-}" ]; then | ||
| return 0 | ||
| fi |
There was a problem hiding this comment.
[Critical] R4-1: (fix-induced) The round-6 fix for R4-1 closed the pre-run-event forgery window — correctly — by requiring the FULL force-push move-then-revert pair inside the START_TS - 15 window, but that same change opened a new defect at this site: a departure made by a normal fast-forward push emits no HEAD_REF_FORCE_PUSHED_EVENT, so the departure side of the pair is invisible, the revert reads as the lone back-push this loop deliberately rejects, and a genuinely superseded-then-restored run falls through to red fail() instead of the clean cede supersede_reverted_during_run exists to grant.
Concrete shape: the run reviews head E; the author pushes commits (fast-forward E→F — no force-push event); the watcher below threshold writes SUPERSEDE_FILE and pkills; the author's corrective force-push back to E lands before the post-attempt re-check. live_head_moved is then false at both re-check sites, this function sees only the F→E event (departed never sets), the lone-back rejection fires, and the job goes red with failure-fallback noise — while the queued replacement still covers head E, so the cost is a spurious red check, not a lost review. The secondary shape holds too: a force-push departure landing between the trigger and START_TS - 15 (queue/setup delay) is filtered by the window with the same result.
Witness (executed): the repo's own test cedes a killed attempt whose superseding push reverted before the re-check — its loneBack arm feeds exactly one timeline line (a lone back-push, precisely the FF-departure shape) and asserts status=1/FAIL at HEAD; this suite codifies the red failure. GitHub timeline of PR 9729 (the incident this machinery exists for): 30 committed events vs exactly 1 head_ref_force_pushed — the modal supersede departure produces no witness event.
Suggested fix: don't source the departure witness solely from force-push events — either broaden the timeline query (pair a during-run commit arrival with the back-push), or make the witness local and unforgeable: the watcher records its kill into a fresh mktemp -d whose path stays in an unexported shell variable (the agent derives every currently-exported signal path, but unexported ones are not in its environment), with the timeline as fallback. If the timeline-only shape stays, widen the tolerance below START_TS to cover trigger-to-step delay, not only clock skew.
Fix witness: extend the cede_revert_kill family with a normal-push departure (STUB_TIMELINE carrying only the back-push line, plus the watcher's kill record) asserting status 0 and Superseded early:; removing the departure detection must turn it red.
中文说明
[Critical] R4-1:(由修复引入)第 6 轮对 R4-1 的修复正确地关闭了运行前事件伪造窗口——要求在 START_TS - 15 窗口内出现完整的 force-push「移走→推回」事件对——但同一改动在该站点引入了新缺陷:普通快进 push 的「移走」不会产生 HEAD_REF_FORCE_PUSHED_EVENT,因此事件对的离开侧不可见,回推被读作本循环刻意拒绝的「单独回推」,真正被取代后又恢复的 run 落入红色 fail(),而非 supersede_reverted_during_run 本应给出的干净让位。
具体形态:run 评审 head E;作者快进推送 E→F(无 force-push 事件);未过阈值的 watcher 写入 SUPERSEDE_FILE 并 pkill;作者的纠错 force-push 在尝试后复查之前推回 E。此时两处复查点的 live_head_moved 均为 false,本函数只看到 F→E 事件(departed 永不置位),单独回推拒绝触发,job 变红并触发失败兜底噪音——而排队的接替 run 仍覆盖 head E,因此代价是多出一个假红色检查,并非丢失评审。次要形态同样成立:落在触发时刻与 START_TS - 15 之间的 force-push 离开(排队/安装延迟)会被窗口过滤,结果相同。
验证证据(已执行):仓库自身测试 cedes a killed attempt whose superseding push reverted before the re-check 的 loneBack 臂恰好只喂一条时间线(单独回推——正是快进离开形态),并在 HEAD 断言 status=1/FAIL;测试套件把该红色失败固化了下来。PR 9729(本机制所针对的事件)的 GitHub 时间线:30 个 committed 事件对 1 个 head_ref_force_pushed——最常见的取代离开形态不产生任何见证事件。
建议修复:离开侧见证不要只取自 force-push 事件——要么扩展时间线查询(运行期间到达的提交与回推配对),要么改用本地不可伪造的见证:watcher 把终止记录写入新的 mktemp -d 目录,其路径保存在未导出的 shell 变量中(agent 能推导出所有已导出的信号路径,但未导出的不在其环境里),时间线作为兜底。若保留纯时间线形态,请将 START_TS 之下的容忍度放宽到覆盖「触发→步骤」延迟,而不仅是时钟偏移。
修复验证:为 cede_revert_kill 族新增快进离开场景(STUB_TIMELINE 仅含回推行,外加 watcher 终止记录),断言状态 0 与 Superseded early:;移除离开侧检测后该测试必须变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| run: |- | ||
| set -euo pipefail | ||
| BODY="$(printf '%s\n' \ | ||
| "<!-- qwen-review-salvaged ${EXPECTED_HEAD_SHA} -->" \ |
There was a problem hiding this comment.
[Critical] R5-3: Still stands — this step posts the note via CI_BOT_PAT as qwen-code-ci-bot, which is exactly REVIEW_BOT (qwen-autofix.yml:87), but the qwen-review-salvaged marker is absent from BOT_COMMENT_FILTER (~:3250) and every sibling exclusion that counts or renders REVIEW_BOT content — so the autofix fleet scan counts the note itself as new actionable feedback. On an autofix-engaged PR whose review salvages after the engagement watermark, the next scan's N_ISSUE_COMMENTS lets the note through (login != AUTOFIX_BOT passes, login == REVIEW_BOT satisfies the author select, the marker matches no exclusion); with a salvaged APPROVE — N_REVIEWS counts only CHANGES_REQUESTED/COMMENTED — the note alone flips "nothing new since watermark" into SELECTED: a full review-address round is dispatched on a bot note, a MAX_TARGETS_PER_SCAN slot burns (deferring a real candidate), and the prepare-leg NEWEST computation advances the watermark to a bot comment. This is the failure class BOT_COMMENT_FILTER's own comment says it exists to prevent; the docs-only-medium note needed its own special-case exclusion for the same defect.
Witness (probe over the real extracted filters at HEAD, fixture = the exact note body posted by this step): N_ISSUE_COMMENTS: note+human = 2, note alone = 1, human alone = 1 | N_REVIEWS for salvaged APPROVE = 0 | NEWEST = the note's timestamp || with qwen-review-salvaged added to the exclusion regex: total = 1, note alone = 0, human still 1.
Suggested fix: add qwen-review-salvaged to BOT_COMMENT_FILTER and the sibling exclusion regexes at ~:4150 (NEWEST), ~:4238 (LIVE_NEW stale-target), ~:4614 and ~:4729 (both agent-feedback renderers, which list the note body to the agent), mirroring the docs-only-medium special case.
Fix witness: add a replay feeding the N_ISSUE_COMMENTS filter a qwen-code-ci-bot comment carrying the salvage marker and assert count 0; removing the new exclusion must turn it red.
中文说明
[Critical] R5-3:仍然存在——本步骤通过 CI_BOT_PAT 以 qwen-code-ci-bot 身份发布说明,而该账号正是 REVIEW_BOT(qwen-autofix.yml:87),但 qwen-review-salvaged 标记不在 BOT_COMMENT_FILTER(~:3250)及任何统计/渲染 REVIEW_BOT 内容的同族排除列表中——因此 autofix 队列扫描会把这条说明本身计为新的可处理反馈。在已启用 autofix 的 PR 上,评审在启用水平线之后被 salvage 时,下一次扫描的 N_ISSUE_COMMENTS 会放行该说明(login != AUTOFIX_BOT 通过、login == REVIEW_BOT 满足作者筛选、标记不匹配任何排除项);若 salvaged 评审为 APPROVE——N_REVIEWS 只统计 CHANGES_REQUESTED/COMMENTED——仅这条说明就能把「水平线之后无新内容」翻转为 SELECTED:一次完整的 review-address 轮次因 bot 说明而被派发,烧掉一个 MAX_TARGETS_PER_SCAN 名额(挤掉真实候选),prepare 段的 NEWEST 计算还会把水平线推进到 bot 评论。这正是 BOT_COMMENT_FILTER 自身注释声明要防止的失败类别;docs-only-medium 说明曾因同一缺陷需要专门的特例排除。
验证证据(对 HEAD 真实提取的过滤器做探针,fixture 为本步骤发布的确切说明正文):N_ISSUE_COMMENTS:说明+人类 = 2,仅说明 = 1,仅人类 = 1 | salvaged APPROVE 的 N_REVIEWS = 0 | NEWEST = 说明的时间戳 || 将 qwen-review-salvaged 加入排除正则后:总数 = 1,仅说明 = 0,人类仍为 1。
建议修复:把 qwen-review-salvaged 加入 BOT_COMMENT_FILTER 及 :4150(NEWEST)、:4238(LIVE_NEW 过时目标)、~:4614 与 ~:4729(两处向 agent 列出说明正文的反馈渲染器)的同族排除正则,参照 docs-only-medium 特例。
修复验证:新增回放,向 N_ISSUE_COMMENTS 过滤器喂一条携带 salvage 标记的 qwen-code-ci-bot 评论并断言计数为 0;移除新排除项后该测试必须变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| format('qwen-pr-review-pr-{0}', github.event.pull_request.number) || | ||
| format('qwen-pr-review-run-{0}', github.run_id) }} | ||
| cancel-in-progress: "${{ github.event_name == 'pull_request_target' && (github.event.action == 'synchronize' || github.event.action == 'closed') }}" | ||
| cancel-in-progress: "${{ github.event_name == 'pull_request_target' && github.event.action == 'closed' }}" |
There was a problem hiding this comment.
[Critical] R5-4: Still stands — dropping synchronize from cancel-in-progress introduced no cross-run dedup on the reviewed head, so an away-and-back force-push that fits inside the watcher's poll gap yields two full reviews and two review posts of the SAME commit. Run A reviews head H; a force-push to X and back to H lands within one SALVAGE_POLL_SECONDS=60 window (or every gh pr view poll fails transiently through it, which the || continue tolerance swallows); the watcher never observes a moved head at decision time, writes no SUPERSEDE_FILE, and run A completes and posts a review of H; the synchronize-queued replacement run then reviews H from scratch and posts a second review of the identical commit. Verified absent at HEAD: neither the delay job's stale-event-head skip (event-head staleness only) nor the review job carries a head-level "already reviewed" lookup.
Witness: not run — requires GitHub Actions concurrency scheduling (pending-slot replacement across live runs), which is not executable off-platform; the absence above is a grep of both workflows at the reviewed commit.
Suggested fix (the shape the round-5 reply queued): head-level dedup before a run starts reviewing — the posted review's machine-ledger marker already embeds the reviewed head SHA, so the delay job's re-check lookup can emit should_review=false when the live head matches an already-posted review's anchor.
Fix witness: a replay of the delay-gate lookup with an already-posted head equal to the live head must assert should_review=false; removing the comparison turns it red.
中文说明
[Critical] R5-4:仍然存在——把 synchronize 从 cancel-in-progress 移除后,并未引入按已评审 head 的跨 run 去重,因此一次落在 watcher 轮询间隙内的「推走又推回」force-push 会对同一个提交产生两次完整评审与两条评审发布。Run A 评审 head H;force-push 到 X 再推回 H 落在一个 SALVAGE_POLL_SECONDS=60 窗口内(或期间每次 gh pr view 轮询都瞬时失败、被 || continue 容忍吞掉);watcher 在决策时刻从未观察到 head 移动、不写 SUPERSEDE_FILE,run A 跑完并发布对 H 的评审;随后 synchronize 排队的接替 run 又从头评审 H 并对同一提交发布第二条评审。已在 HEAD 确认缺失:delay job 的 stale-event-head 跳过只判断事件 head 过时,评审 job 也没有任何 head 级「已评审」查询。
验证证据:未运行——需要 GitHub Actions 并发调度(跨活跃 run 的排队位替换),平台外不可执行;上述缺失结论为对两个工作流在受审提交上的 grep。
建议修复(第 5 轮答复已排队的形态):评审开始前做 head 级去重——已发布评审的机器账本标记已内嵌受审 head SHA,delay job 的复查查询可在 live head 与已发布评审锚点相同时输出 should_review=false。
修复验证:新增 delay 闸门查询回放,使已发布 head 等于 live head,断言 should_review=false;移除该比较后测试必须变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| if [ ! -f "${SALVAGE_DIR}/compose-seen" ] && [ -f "$COMPOSED_ARTIFACT" ] \ | ||
| && node -e 'const v=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));if(typeof v!=="object"||v===null||Array.isArray(v))process.exit(1);' "$COMPOSED_ARTIFACT" 2>/dev/null; then |
There was a problem hiding this comment.
[Critical] R6-3: Still stands — the JSON-parse validation that closed R2-2 introduced a blocking re-open of the same agent-derivable path: the latch tests [ -f ] on $COMPOSED_ARTIFACT, then node -e …readFileSync REOPENS it. The [ -f ] gate defeats a statically planted FIFO, but not a rename-swap inside the check-then-open window — a FIFO swapped in after the test blocks readFileSync forever with no writer and no timeout, permanently killing the supersede watcher: every later head move of this run goes unwatched (CEDE never fires; a stale-head run burns the remaining budget and posts, or a moved-head run fails red where it should have ceded). The path is fully derivable by the reviewed agent (cwd + PR number — the diff's own comment acknowledges agent control), and the round-2/R6-3 deferral queued exactly this timeout-bounded reopen but it is not in this commit.
Witness (probe, disposable dir, FIFO planted at the artifact path, the latch's verbatim read): timeout 5 node -e '…readFileSync…' → exit 124 — killed by the timeout; the open never returned.
| if [ ! -f "${SALVAGE_DIR}/compose-seen" ] && [ -f "$COMPOSED_ARTIFACT" ] \ | |
| && node -e 'const v=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));if(typeof v!=="object"||v===null||Array.isArray(v))process.exit(1);' "$COMPOSED_ARTIFACT" 2>/dev/null; then | |
| if [ ! -f "${SALVAGE_DIR}/compose-seen" ] && [ -f "$COMPOSED_ARTIFACT" ] \ | |
| && timeout 5 node -e 'const v=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));if(typeof v!=="object"||v===null||Array.isArray(v))process.exit(1);' "$COMPOSED_ARTIFACT" 2>/dev/null; then |
(or open-once semantics: read the file into a variable first, then parse) — plus document the residual forgery risk the way the marker escape does.
Fix witness: a runWatcher case that swaps the artifact to a FIFO between poll and read must assert the watcher still reaches a bounded decision (no hang); removing the timeout turns it into a harness timeout.
中文说明
[Critical] R6-3:仍然存在——关闭 R2-2 的 JSON 解析校验在同一条 agent 可推导路径上引入了阻塞式重新打开:锁存先对 $COMPOSED_ARTIFACT 做 [ -f ] 检查,随后 node -e …readFileSync 又重新打开该路径。[ -f ] 闸门能防住静态放置的 FIFO,却防不住检查与打开之间窗口内的 rename 换入——在检查通过之后换入 FIFO,readFileSync 会在没有写端、没有超时限制的情况下永久阻塞,从而永久杀死 supersede watcher:此后该 run 的任何 head 移动都无人监视(CEDE 永不触发;过期 head 的 run 烧完剩余预算后发布,或本应让位的移动 head run 变红失败)。该路径可被评审 agent 完全推导(cwd + PR 编号——diff 自己的注释也承认 agent 可控),第 2 轮 / R6-3 的延后清单恰好排队了这个「超时有限的重新打开」,但本提交并未包含。
验证证据(探针,一次性目录,工件路径放置 FIFO,逐字执行锁存读取):timeout 5 node -e '…readFileSync…' → 退出码 124——被超时杀死,打开操作始终未返回。
建议修复:为该读取加 timeout 5(上方 suggestion),或改为打开一次语义(先把文件读入变量再解析),并像 marker 逃生口那样文档化残余伪造风险。
修复验证:新增 runWatcher 用例,在轮询与读取之间把工件换成 FIFO,断言 watcher 仍能得到有界决策(不挂起);移除超时后该用例将变成测试框架超时(变红)。
— qwen3.8-max via Qwen Code /review (v0.22.2)
|
🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x), Post Coverage Comment (ubuntu-latest, 22.x)] pass on current main — merged current main via update-branch; CI will re-run. 中文说明🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x), Post Coverage Comment (ubuntu-latest, 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。 |
|
🤖 Addressed the latest review feedback (round 3/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 3/100 轮)。改动内容与我反驳保留之处如下: Round summary — PR #10123 (address-review, round 8)All five Critical findings are resolved in code. No conflict resolution was needed ( Feedback points and dispositionsR7-1 — SUPERSEDE_FILE cede checks ungated on AUTO_REVIEW (rc:3875755016) — RESOLVEDThe two
R4-1 — FF-departure supersede-then-revert falls through to red (rc:3875755038) — RESOLVEDImplemented the finding's suggested local-witness shape. When the watcher is armed, the step mints a fresh
R5-3 — salvage note counted as actionable fleet feedback (rc:3875755046) — RESOLVEDAdded
R5-4 — no head-level dedup for the same reviewed commit (rc:3875755051) — RESOLVEDImplemented the finding's suggested shape: the delay job's re-check now looks up the PR's reviews, keeps the bot-authored ones carrying a
R6-3 — compose-latch reopen can wedge the watcher forever (rc:3875755062) — RESOLVEDApplied the finding's suggested fix: the latch's
Not worked this roundThe review body's VerificationCommands actually run and their results:
Mutation probes (each guard removed → its witness fails → guard restored → green):
Integration tests after 中文说明轮次总结 — PR #10123(address-review,第 8 轮)五条 Critical 发现均已在代码中解决。本轮无需处理冲突( 反馈点与处置R7-1 — SUPERSEDE_FILE 让位检查未加 AUTO_REVIEW 闸门(rc:3875755016)— 已解决两处
R4-1 — 快进离开的「取代后恢复」落入红色失败(rc:3875755038)— 已解决实现了发现建议的本地见证形态。watcher 布防时,步骤会新建一个
R5-3 — salvage 说明被计为可处理的队列反馈(rc:3875755046)— 已解决在发现点名的五个站点把
R5-4 — 同一已评审提交缺少 head 级去重(rc:3875755051)— 已解决实现了发现建议的形态:delay job 的复查现在查询 PR 的评审,保留带
R6-3 — compose 锁存的重新打开可能永久卡死 watcher(rc:3875755062)— 已解决采用发现建议的修复:锁存的
本轮未处理评审正文中 验证实际执行的命令与结果:
变异探针(每个守卫移除 → 其见证测试失败 → 恢复守卫 → 变绿):
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
7 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- design-doc test pins af-149 instead of af-151 — already reported as round-7 deferral D7-2 (review 5045544185)
- wrapper salvage-marker planted-FIFO witness — already reported as R4-5 (comments 3869272397 / 3872716975, recorded deferred in the round-4/6/7 bodies)
- report-time hold blind to watcher-less command runs — already reported as round-7 deferral D7-5 (review 5045544185)
- post-loop salvaged outputs block missing AUTO_REVIEW gate — already reported as round-7 deferral D7-1 (review 5045544185)
- docs-only-medium salvage KEEP dead end — already reported as round-1 open comment 3862832343
- stale cancel wording at the hold echo / ack-on-defer comment — already reported as the round-1 finding (comment 3862832385, accepted and deferred)
- salvage reset/trap rm -rf calls lack || true — already reported as round-7 deferral D7-6 (review 5045544185)
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted; the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
Deferred under the convergence posture (round 8, not a blocker) — recorded, not requested in this round:
.github/workflows/qwen-code-pr-review.yml:1410 — [probe] compose latch authenticates shape, not provenance — forged {} forces KEEP.github/workflows/qwen-code-pr-review.yml:1726 — [probe] attempt-start guard admits planted 1 — always-KEEP at default pct.github/workflows/qwen-code-pr-review.yml:1687 — [review] watcher self-bound misses early-hard-kill leaks; replacement reachable via PR-scoped REVIEW_URL.github/workflows/qwen-code-pr-review.yml:1751 — [review] cede-kill orphans the agent's detached URL-less tool subprocessesscripts/tests/qwen-pr-review-workflow.test.js:310 — [probe] runScenario timeout stub mangles the new compose-latch call shape (latent).github/workflows/qwen-code-pr-review.yml:1630 — [probe] write_signal mv -f moves INTO a planted directory — silent write loss forces KEEPscripts/tests/qwen-pr-review-workflow.test.js:4317 — [probe] runWatcher readOr blocks forever on a surviving planted FIFOscripts/tests/qwen-pr-review-workflow.test.js:4098 — [probe] armWatcher replays leak enumerable kill dirs into the lane's RUNNER_TEMPscripts/tests/qwen-pr-review-workflow.test.js:5069 — [probe] retry reset's killed-record removal has no witness — stale record suppressesscripts/tests/qwen-pr-review-workflow.test.js:203 — [probe] gh stub scripts by first-caller order — salvage_armed_then_die racescripts/tests/qwen-autofix-workflow.test.js:23206 — [probe] design-doc pin asserts af-149 instead of the new af-151 anchor.github/workflows/qwen-code-pr-review.yml:2031 — [review] note step consumes agent-appendable GITHUB_OUTPUTscripts/tests/qwen-pr-review-workflow.test.js:5201 — [probe] SUPERSEDE_KILL_DIR trap cleanup clause exercised by no test
Convergence: round 8 posted 6 inline comment(s), 6 of them reported for the first time; the previous round posted 5 (2 new). Findings keep coming back to the same files: .github/workflows/qwen-code-pr-review.yml (findings in rounds 4, 5, 6, 7; 6 more now). The rate of new findings is not falling. A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. Batching the remaining fixes and verifying them before the next push keeps the loop from re-deriving the same set; this PR's reviews already resolve to a critical posting floor. (Observation only — nothing was withheld from this review because of this observation.)
Residual risk: this loop is persistently critical — Criticals stood in the previous round's work-list and stand again this round (6 Critical(s)), the rate of first-time findings is not falling (this round 6, previous 2), and the standing Critical backlog is not shrinking. The severity floor will not converge it. Recommendation: land-with-residual-risk — the exit is a maintainer risk-acceptance decision (merge, carrying the residual risk), not another review round. Residual-risk inventory for that decision (maintainer to complete):
| standing Critical | attack surface | attacker-dependency | blast radius |
|---|---|---|---|
| (each standing Critical) | … | … | … |
Advisory only — it does not block this review.
中文说明
本轮确认的 7 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted; the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
收敛姿态下延后(第 8 轮,非阻断)——已记录,本轮不要求修改:共 13 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 8 轮发布了 6 条行内评论,其中 6 条是首次提出;上一轮发布了 5 条(其中 2 条首次提出)。发现反复回到同一批文件:.github/workflows/qwen-code-pr-review.yml(第 4、5、6、7 轮已出过发现,本轮又有 6 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,可以避免循环反复推导同一组发现;本 PR 的评审已解析为 critical 发布下限。(仅为观察——本轮评审未因此扣留任何内容。)
残余风险:本循环处于 persistently-critical 形态——上一轮工作清单中的 Critical 本轮依然存在(本轮 6 条 Critical),首次发现的速率没有下降(本轮 6,上一轮 2),且未决 Critical 积压没有减少。severity floor 无法使其收敛。建议:land-with-residual-risk——出口是 maintainer 的风险接受决定(合入并承担残余风险),而非再开一轮评审。供该决定使用的残余风险清单(maintainer 填写):按每条未决 Critical 列出「攻击面 · 攻击者依赖性 · 影响范围」三栏。仅为建议——不阻断本次评审。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| if [ -n "${SUPERSEDE_KILL_DIR:-}" ] && [ -f "${SUPERSEDE_KILL_DIR}/killed" ]; then | ||
| return 0 | ||
| fi |
There was a problem hiding this comment.
[Critical] R4-1: (fix-induced) The round-8 fix for R4-1 closed the fast-forward-departure gap by adding this kill record as supersede_reverted_during_run's primary witness, but the record is forgeable. SUPERSEDE_KILL_DIR is minted with mktemp -d under the enumerable RUNNER_TEMP with the fixed prefix qwen-review-supersede-kill.* — "never exported" prevents derivation, not discovery: the reviewed agent runs same-uid (the watcher's own pkill -U "$(id -u)" relies on exactly that), can list the directory, and the 0700 mode does not bar a same-uid write. The check here is a bare [ -f ] with no content validation. An agent that plants killed here plus ${SALVAGE_DIR}/superseded (derivable from the exported QWEN_CI_REVIEW_SALVAGE_OK_FILE) and then fails the attempt fatally (a fatal outcome skips the retry reset that deletes planted files) makes the last-look gate hit this branch and return 0 before any live-head re-check — with the head never moved — ceding exit 0 green: no review posted, no failure-fallback comment, no replacement run queued. That is silent suppression of the entire review, the exact class the sibling cede sites' live-head re-checks exist to prevent.
Witness (probe over the verbatim-extracted gate; stub gh reports an UNMOVED head, silent timeline):
armed: SUPERSEDE_KILL_DIR=…/qwen-review-supersede-kill.ajcEwV (mode 700, same uid)
agent planted: …/killed + ${SALVAGE_DIR}/superseded
last-look (live head UNMOVED): 'Superseded early: … ceding to the replacement run' — exit 0
mutant (kill-record branch disabled): FAIL kind=[fatal] — exit 1 ← the branch is decisive
A filesystem location is not a trust boundary against a same-uid agent. Either drop this early return and keep only the server-side timeline pair (a fast-forward departure followed by a revert then stays a visible red failure — the diff itself names that the safe default), or bind the record to a nonce minted in the step shell (never exported) and require content equality, and/or re-check the live head in this same gate like every sibling cede.
Fix witness: a replay in scripts/tests/qwen-pr-review-workflow.test.js that plants both the supersede file and a killed record from the agent side (watcher not armed, live head == expected head, silent timeline) must assert status 1 with FAIL and no Superseded early: — removing the gate must turn it red.
中文说明
[Critical] R4-1:(由修复引入)本轮针对 R4-1 的修复将该 kill 记录加为 supersede_reverted_during_run 的主要见证,封闭了 fast-forward 离开不可见的缺口,但该记录本身可伪造。SUPERSEDE_KILL_DIR 以固定前缀 qwen-review-supersede-kill.* 在可枚举的 RUNNER_TEMP 下由 mktemp -d 生成——「从不导出」只防推导、不防发现:被评审 agent 以同一 uid 运行(可以列目录,0700 不阻挡同 uid 写入),而此处检查是不带内容校验的裸 [ -f ]。agent 在此放置 killed,再放置 ${SALVAGE_DIR}/superseded(可由导出的 QWEN_CI_REVIEW_SALVAGE_OK_FILE 推导),随后让尝试以 fatal 方式失败(fatal 结果会跳过删除放置文件的重试重置),最后检查处该分支就会在任何 live-head 复核之前返回 0——即使 head 从未移动——让位后以 exit 0 绿色结束:不发布评审、无失败兜底评论、也无接替 run。这是整次评审的静默压制,正是同族让位点的 live-head 复核所要防止的类别。验证证据(对逐字提取的闸门做探针;stub gh 报告 head 未移动、时间线静默):放置后输出 'Superseded early'、exit 0;禁用该分支的突变体输出 FAIL、exit 1——该分支是决定性的。文件系统位置对同 uid agent 不构成信任边界:要么移除该提前返回、仅保留服务端时间线对(fast-forward 离开后回退将仍表现为可见的红色失败——diff 自己也承认这是安全默认);要么将记录绑定到步骤 shell 内生成的 nonce(不导出)并要求内容相等;和/或像所有同族让位点一样在同一闸门内复核 live head。修复验证:新增回放——从 agent 侧同时放置 supersede 文件与 killed 记录(watcher 未布防、live head == 期望 head、时间线静默),断言 status 1 且输出 FAIL、无 Superseded early:;移除该闸门后必须变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| # review, a false skip would lose one. | ||
| reviewed_shas="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews" --paginate \ | ||
| --arg bot "qwen-code-ci-bot" \ | ||
| --jq '.[] | select((.user.login // "") == $bot) | (.body // "") | select(contains("<!-- qwen-review-ledger ")) | capture("\"sha\":\"(?<sha>[0-9a-f]{7,64})\"")? | .sha' 2>/dev/null)" || reviewed_shas="" |
There was a problem hiding this comment.
[Critical] R5-4: (fix-induced) The round-8 fix for R5-4 (head-level dedup) extracts the reviewed head from bot review bodies with a FIRST-MATCH capture of "sha":"…" — but the body is content the reviewed agent authors and posts through a gh guard that never inspects bodies, and the sha is never authenticated. Two witnessed forgery shapes both manufacture the false skip this block's own comment says fail-open exists to prevent ("a false skip would lose one"). Shape 1: git permits " in filenames, and mdField strips backticks/newlines/comment grammar but keeps double quotes and hex — a file named x"sha":"<sha-of-B>".ts in commit A renders into the bot body before the genuine marker; the author then pushes B (whose sha they know — it is their own commit) and the queued run for B skips itself. Shape 2: a forged <!-- qwen-review-ledger … --> marker placed before the genuine one in the posted body wins the capture outright — so anchoring the capture to the marker opener does NOT fix this; the sha INSIDE a forged marker is attacker-influenced. The CLI's own parseLedger deliberately reads the LAST marker (lastIndexOf: "an edited or quote-carrying body can hold more than one") — this dedup does the opposite.
Witness (executed the workflow's exact --jq program vs a LAST-marker variant over constructed bot review bodies):
bare "sha":"H2" before genuine marker: first-match → forged H2 wins | LAST-marker → genuine H1
full forged ledger marker before genuine: first-match → forged H2 wins | LAST-marker → genuine H1
control (clean body): both return genuine H1
delay-step decision: should_review=false (forged) ↔ true (last-marker) — flips
Parse only the LAST ledger marker per body (mirror the CLI's convention, since compose appends the genuine marker last), and reject skips whose marker sha disagrees with a server-side attribute — fail-open, never a skip. Note commit_id alone cannot be the key: a salvaged historical-head review posts while a different head is live, so GitHub records the live head. Longer-term, move the dedup record off the agent-authored body entirely (a workflow-written record in the posting step).
# sketch — take the LAST marker per body, then the sha inside it:
--jq '.[] | select((.user.login // "") == $bot) | (.body // "")
| [capture("<!-- qwen-review-ledger [^\n]*" ; "g")]? | last // empty'
# then cross-check against a server-side attribute before skipping; never skip on parse failure.Fix witness: a delay-job suite case whose bot review carries a first ledger-shaped sha naming the live head while the genuine (last) marker names a different sha must yield should_review=true; removing the last-marker rule must turn it red.
中文说明
[Critical] R5-4:(由修复引入)本轮针对 R5-4 的修复(head 级去重)以「首个匹配」的 capture 从机器人评审正文中提取被评审 head——但正文由被评审 agent 撰写、并经一个从不检查正文的 gh 守卫发布,sha 从未被认证。两种已被见证的伪造形态都能制造该代码块注释自己声明要防止的「错误跳过」(「漏去重只损失一次重复评审,错误跳过会损失一次评审」)。形态 1:git 允许文件名包含 ",而 mdField 只剥离反引号/换行/注释语法、保留双引号与十六进制——在提交 A 中加入名为 x"sha":"<B的sha>".ts 的文件即可渲染进正文、位于真标记之前;作者随后推送自己的提交 B(其 sha 已知),为 B 排队的 run 便跳过自己。形态 2:在已发布正文中把伪造的 <!-- qwen-review-ledger … --> 放在真标记之前即可直接赢得首个匹配——因此把 capture 锚定到标记开头并不能修复此问题:伪造标记内部的 sha 本身受攻击者控制。CLI 自己的 parseLedger 刻意读取最后一个标记(lastIndexOf:「被编辑或带引用的正文可能含多个标记」)——此去重逻辑恰好相反。验证证据(对构造正文分别执行工作区原样的 --jq 与「最后标记」变体):两种伪造形态下首匹配都返回伪造 H2,最后标记解析都返回真 H1,delay 步骤决策随之翻转。修复方向:每个正文只解析最后一个标记(与 CLI 约定一致,compose 最后追加真标记),并在跳过前与服务器端属性交叉校验——失败时放行、绝不跳过。注意单靠 commit_id 不能作为键:历史 head 的 salvage 评审发布时 live head 已不同。更长期:把去重记录移出 agent 撰写的正文(由发布步骤写入工作流自有记录)。修复验证:delay 套件新增用例——机器人正文的首个账本形态 sha 指向 live head、而真(最后)标记指向不同 head,必须得到 should_review=true;移除最后标记规则后必须变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| # would otherwise block readFileSync forever (no writer) and | ||
| # kill the watcher permanently. | ||
| if [ ! -f "${SALVAGE_DIR}/compose-seen" ] && [ -f "$COMPOSED_ARTIFACT" ] \ | ||
| && timeout 5 node -e 'const v=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));if(typeof v!=="object"||v===null||Array.isArray(v))process.exit(1);' "$COMPOSED_ARTIFACT" 2>/dev/null; then |
There was a problem hiding this comment.
[Critical] R6-3: (fix-induced) The round-8 fix for R6-3 bounded this compose-latch reopen with timeout 5, which introduces a host timeout(1) dependency the replay harness does not provide: runWatcher's stub bin supplies only sleep/gh/pkill/(node) — no timeout stub, although its sibling runScenario has one. The suite runs on the test_macos lane (scripts/tests/vitest.config.ts excludes it only on win32, and macOS ships no GNU timeout), gated to merge_group/schedule/workflow_dispatch — so it reports skipped on this PR and the first red lands in the merge queue.
Witness (reproduced on the Linux lane by shadowing timeout with an exit-127 executable, i.e. the macOS shape):
× latches the compose signal only from a real composed artifact (replayed watcher)
AssertionError: expected null to be 'head-a' (test file line ~4415)
Tests 2 failed | 45 passed (unshadowed: green)
latches the compose signal only from a real composed artifact fails because timeout: command not found (127) means compose-seen never latches and the watcher CEDEs; bounds the compose-artifact reopen against a rename-swapped FIFO passes VACUOUSLY on that lane — the latch call fails before the swap, so the 5s bound this test exists to pin is never exercised there.
Add a timeout stub to runWatcher's stub bin that ENFORCES the duration (parse the leading seconds, spawn the rest, kill after the bound, exit 124) — e.g. node-based, since node exists on every lane this suite runs. A bare pass-through stub is not sufficient: measured — it unbounds the FIFO-swap hazard and the reopen test then hangs to the harness's own 30s bound.
Fix witness: the valid-artifact arm of latches the compose signal only from a real composed artifact (replayed watcher) goes red on a timeout-less lane without the stub; bounds the compose-artifact reopen against a rename-swapped FIFO pins that the stub still enforces the bound.
中文说明
[Critical] R6-3:(由修复引入)本轮针对 R6-3 的修复用 timeout 5 为该 compose 锁存的重新打开加了界,由此引入了对宿主 timeout(1) 的依赖,而回放 harness 并未提供:runWatcher 的 stub bin 只提供 sleep/gh/pkill/(node),没有 timeout stub(其姊妹 runScenario 却有)。该套件在 test_macos 泳道运行(scripts/tests/vitest.config.ts 仅在 win32 排除,且 macOS 不自带 GNU timeout),该泳道由 merge_group/schedule/workflow_dispatch 触发——在本 PR 页面显示为跳过,首个红色将在合并队列中暴露。验证证据(在 Linux 泳道用一个 exit-127 的 timeout 影子复现,即 macOS 形态):'latches the compose signal only from a real composed artifact' 以 expected null to be 'head-a' 失败(2 failed | 45 passed;无影子时全绿)。原因:timeout: command not found(127)导致 compose-seen 永不锁存、watcher 让位;'bounds the compose-artifact reopen against a rename-swapped FIFO' 在该泳道空过——锁存在 swap 之前就已失败,该测试本要钉住的 5 秒界从未被执行。修复:在 runWatcher 的 stub bin 中加入一个强制时限的 timeout stub(解析秒数、启动子进程、到点 kill、exit 124,可用 node 实现),裸透传 stub 不够——实测会解除 FIFO 危害的界限、使重开测试挂到 harness 自身的 30 秒界。修复验证:无 stub 的泳道上上述有效工件分支变红;rename-swap FIFO 测试钉住 stub 仍强制时限。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| # kill the watcher (epoch seconds start with 0 only past 2286). | ||
| attempt_start="" | ||
| if [ -f "${SALVAGE_DIR}/attempt-start" ]; then | ||
| attempt_start="$(cat "${SALVAGE_DIR}/attempt-start" 2>/dev/null || true)" |
There was a problem hiding this comment.
[Critical] R8-10 (1 of 3 locations): Salvage-signal reads gate on [ -f ] and then reopen the agent-derivable path with an UNBOUNDED cat — the exact check-then-open shape the diff bounds with timeout 5 for the compose latch one block away, and the shape the round-6 deferral reply queued ("timeout-bounded reopens (compose-latch node read, attempt-start and read_head_signal cats)") of which only the compose-latch half landed. This site: the watcher's attempt-start read. A FIFO rename-swapped into the [ -f ]→cat window blocks with no writer and no timeout: the watcher's one-shot decision dies permanently — no cede, no salvage arming — so on a real head move the run burns the entire remaining budget re-reviewing the dead head while the queued replacement waits, and posts a stale-head review at the end. A live attacker exists: the reviewed agent (same uid, SALVAGE_DIR derivable from its exported env) can race the window on any poll after a head move. head -c 64 also closes the static huge-file plant at the sibling read_head_signal site (location 3).
Witness (probe driving the verbatim-extracted watcher with a cat stub that rename-swaps a FIFO in at open time — the PR's own swapArtifactOnRead technique):
[control] no swap: decided in 24ms superseded=head-b pkilled=true
[BUG] FIFO swapped at open: wedged the whole window — no cede, no kill
[fix] timeout 5 cat: decided in ~5s superseded=head-b pkilled=true
Bound the read the way the compose latch is bounded:
| attempt_start="$(cat "${SALVAGE_DIR}/attempt-start" 2>/dev/null || true)" | |
| attempt_start="$(timeout 5 head -c 64 "${SALVAGE_DIR}/attempt-start" 2>/dev/null || true)" |
Fix witness: extend the planted-FIFO replay family in scripts/tests/qwen-pr-review-workflow.test.js with a swap-at-open variant for this read; it must decide inside the harness bound, and removing the bound must turn it into a harness timeout. Note: runWatcher's stub bin needs the bounded timeout stub from the R6-3 finding first.
中文说明
[Critical] R8-10(3 处之 1):salvage 信号读取先以 [ -f ] 把关、随后用无界的 cat 重新打开 agent 可推导的路径——正是 diff 在上一代码块为 compose 锁存用 timeout 5 加界的 check-then-open 形态,也是第 6 轮延后回复中排队要修的形态(「timeout 有界的重新打开:compose 锁存的 node 读取、attempt-start 与 read_head_signal 的 cat」),而其中只有 compose 锁存那一半落地了。本站点:watcher 的 attempt-start 读取。在 [ -f ]→cat 窗口内被 rename 换入的 FIFO 会无写者、无时限地阻塞:watcher 的一次性决策永久死亡——不让位、不布防 salvage——真实 head 移动时,run 将在整个剩余预算内重审死 head,排队的接替只能等待,最终发布过期 head 评审。存在活体攻击者:被评审 agent(同 uid,SALVAGE_DIR 可由其导出环境推导)可在 head 移动后的任一次轮询中竞争该窗口。验证证据(用开读时 rename 换入 FIFO 的 cat stub 驱动逐字提取的 watcher):对照 24ms 决策(让位+kill);换入 FIFO 则整个窗口卡死;timeout 5 cat 修复后约 5 秒决策。修复:按 compose 锁存的方式加界(上方 suggestion)。修复验证:在植入 FIFO 回放家族中为本站点新增开读时 swap 变体,必须在 harness 界限内完成决策;移除界限后应变为 harness 超时。注意:runWatcher 的 stub bin 需先补上 R6-3 所述的有界 timeout stub。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| # block this cat — and with it the posting path — forever. | ||
| if [ -n "${QWEN_CI_REVIEW_SALVAGE_OK_FILE:-}" ] \ | ||
| && [ -f "${QWEN_CI_REVIEW_SALVAGE_OK_FILE}" ] \ | ||
| && [ "$(cat "${QWEN_CI_REVIEW_SALVAGE_OK_FILE}" 2>/dev/null)" = "$expected_head" ]; then |
There was a problem hiding this comment.
[Critical] R8-10 (2 of 3 locations): Same unbounded check-then-open pattern as the attempt-start and read_head_signal sites, here on the POSTING path: the salvage escape's marker read gates on [ -f ] then reopens with an unbounded cat. The site's own comment concedes the outcome ("a planted FIFO would block this cat — and with it the posting path — forever") while relying on [ -f ], which refuses only a STATIC plant; the rename-swap window between the test and the open is precisely what the diff bounds with timeout 5 one function away. The attacker is live here by construction: posting rides the agent's own gh call (the PROXY_BIN shim runs as a child of the agent invocation, so the agent tree is alive during this read), the marker path is exported in the agent's environment, and sanitize-child-env strips only three internal tokens.
Witness (probe, LD_PRELOAD constructor rename-swapping a FIFO onto the marker inside cat's startup — i.e. after [ -f ] already passed):
wedge arm: marker read hung — killed by timeout, exit 124
control arm: 'PR write allowed (salvage)' — exit 0
fixed arm: timeout 5 head -c 128 under wedge → 'Blocked PR write', exit 90 in ~5s
After the attempt budget bleeds, OUTCOME=timeout meets the salvage-armed check and cedes GREEN — silently discarding a finished review minutes from posting, the exact loss #10110 exists to prevent, with no failure signal at all. Bound the read like the compose latch (fail closed on timeout — the escape is a correctness gate):
salvage_pin="$(timeout 5 head -c 128 "${QWEN_CI_REVIEW_SALVAGE_OK_FILE}" 2>/dev/null || true)"
if [ -n "$salvage_pin" ] && [ "$salvage_pin" = "$expected_head" ]; thenFix witness: a guard-replay case mirroring the compose FIFO test — swap a FIFO onto the salvage-ok path at open time during a salvaged write and assert the write resolves bounded (exit 90 in ~5s), not a harness hang.
中文说明
[Critical] R8-10(3 处之 2):与 attempt-start 及 read_head_signal 站点相同的无界 check-then-open 形态,此处位于发布路径:salvage 逃生口的标记读取先 [ -f ] 把关、再用无界 cat 重开。该站点自己的注释已承认后果(「植入的 FIFO 会阻塞这次 cat——连带发布路径——直到永远」),却依赖只能拒绝静态植入的 [ -f ];测试与打开之间的 rename-swap 窗口正是 diff 在相邻函数用 timeout 5 加界的对象。此处攻击者按构造就是活体:发布经由 agent 自己的 gh 调用(PROXY_BIN 垫片作为 agent 调用的子进程运行,读取时 agent 树仍在),标记路径已导出到 agent 环境,且 sanitize-child-env 只剥离三个内部 token。验证证据(LD_PRELOAD 构造器在 [ -f ] 通过后、cat 启动时换入 FIFO): wedge 臂挂起至被杀(exit 124);对照臂 'PR write allowed (salvage)' exit 0;修复臂(timeout 5 head -c 128)约 5 秒得到 'Blocked PR write' exit 90。尝试预算耗尽后 OUTCOME=timeout 遇上 salvage 布防检查并绿色让位——静默丢弃一份距发布仅差几分钟的已完成评审,正是 #10110 要消除的损失,且毫无失败信号。按 compose 锁存的方式加界(超时即失败关闭——逃生口是正确性闸门)。修复验证:守卫回放新增用例——salvage 写入期间在打开时刻换入 FIFO,断言写入有界解决(约 5 秒 exit 90),而非 harness 挂起。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| read_head_signal() { | ||
| local content='unknown' | ||
| if [ -f "${1:-}" ]; then | ||
| content="$(cat "$1" 2>/dev/null || echo unknown)" |
There was a problem hiding this comment.
[Critical] R8-10 (3 of 3 locations): Same unbounded check-then-open pattern inside read_head_signal, serving BOTH its call sites: cede_superseded's SUPERSEDE_FILE read (:1799) and the post-loop moved-to read (:1973). Two mechanisms: (a) the rename-swap wedge — a FIFO swapped into the [ -f ]→cat window blocks the cede exit itself, holding the run open to the job timeout and delaying the queued successor; (b) a race-FREE size exhaustion — a multi-GB regular file passes [ -f ], and $(cat "$1") slurps the whole of it into command substitution (no cap anywhere on the path): measured 400MB in 13s; a multi-GB plant OOMs the runner. The post-loop site runs after the agent exits, but nothing on the KEEP/success path kills the agent's children — detached tool subprocesses survive parent exit (the CLI spawns them detached, URL-less argv), inherit the exported signal paths, and can plant any time between watcher arming and this read.
Witness (probe over the verbatim read_head_signal):
wedge arm: FIFO rename-swapped at open → exit 124 at the 10s bound (hang)
size arm: 400MB plant → RESULT=unknown exit=0 took 13s (unbounded slurp)
fixed arm: timeout 5 head -c 64 → RESULT=unknown took 0s
Bound both dimensions in this one shared function (40-hex is the only accepted content, so head -c caps size and timeout caps the open):
| content="$(cat "$1" 2>/dev/null || echo unknown)" | |
| content="$(timeout 5 head -c 64 "$1" 2>/dev/null || echo unknown)" |
Fix witness: extend the 'never blocks a signal write or read on a planted FIFO' family with swap-at-open and huge-file variants for the superseded/moved-to reads; each must complete inside the harness bound, and removing the bounds must turn them into harness timeouts.
中文说明
[Critical] R8-10(3 处之 3):read_head_signal 内部相同的无界 check-then-open 形态,同时波及其两个调用点:cede_superseded 的 SUPERSEDE_FILE 读取(约 :1799)与循环后的 moved-to 读取(约 :1973)。两种机制:(a) rename-swap 楔入——在 [ -f ]→cat 窗口换入 FIFO 会阻塞让位退出本身,把 run 拖到 job 超时、拖延排队的接替;(b) 无需竞争的体积耗尽——数 GB 的普通文件可通过 [ -f ],$(cat "$1") 会把整个文件吞进命令替换(路径上没有任何上限):实测 400MB 耗时 13 秒,数 GB 植入会 OOM。循环后站点虽在 agent 退出后运行,但 KEEP/成功路径不会杀 agent 的子进程——detached 工具子进程在父进程退出后存活(CLI 以 detached、无 URL 的 argv 派生它们)、继承导出的信号路径,可在 watcher 布防到本次读取之间的任何时刻植入。验证证据(对逐字 read_head_signal 做探针):楔入臂 exit 124(挂起至界限);400MB 臂 13 秒无限吞读;timeout 5 head -c 64 修复臂即时返回。在这一共享函数内同时加界两个维度(40 位十六进制是唯一被接受的内容,head -c 限体积、timeout 限打开;上方 suggestion)。修复验证:在植入 FIFO 回放家族中为 superseded/moved-to 读取新增开读时 swap 与巨型文件变体,必须在 harness 界限内完成;移除界限后变为 harness 超时。
— qwen3.8-max via Qwen Code /review (v0.22.2)
|
🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run. 中文说明🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
3 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- design-doc test pin asserts af-149 instead of af-151 — already reported as round-7 deferral D7-2 (review 5045544185, re-recorded in the round-8 deferral list)
- report-step hold fail-open probe fallbacks untested — already reported in the round-4 deferral list ('hold tests pin jq filters but not the ROLLUP_R/LIVE_HEAD_R fetches', review 5037862190)
- size-baseline stale against the PR's own HEAD — already reported as R2-9 (comment 3865698450) and R5-7 (deferred in the round-6 body, review 5043372802)
Not reviewed: reverse audit — stopped at the 5-round cap without converging (round 5 still reported findings).
Not reviewed: build-and-test — Test (macos-latest, Node 22.x) was skipped in CI (merge_group-gated) and the suite ran locally on Linux only; the macOS-lane failure mode is finding R6-3 itself.
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted; the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
Deferred under the convergence posture (round 9, not a blocker) — recorded, not requested in this round:
scripts/tests/qwen-pr-review-workflow.test.js:5419 — [review] D9-1 — delay-step dedup replay is ungated on host jq (bare it() breaks the suite's hasJq convention; phantom red on jq-less hosts)scripts/tests/qwen-pr-review-workflow.test.js:5241 — [review] D9-2 — salvage-note never-fail tolerance ( || echo "::warning::" ) is pinned by no test
Convergence: round 9 posted 4 inline comment(s), 1 of them reported for the first time; the previous round posted 6 (6 new). Findings keep coming back to the same files: .github/workflows/qwen-code-pr-review.yml (findings in rounds 4, 5, 6, 8; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)
[Critical] R8-1 still stands — .github/workflows/qwen-code-pr-review.yml:1723 — the unbounded check-then-open on the attempt-start read is re-verified at this commit (code unchanged since round 8; rediscovered this round) but is NOT re-posted inline: its round-8 thread already occupies that exact location (comment 3879270374, posted as 'R8-10 (1 of 3 locations)'). The blocker remains open in that thread.
[Critical] R8-2 still stands — .github/workflows/qwen-code-pr-review.yml:1106 — the unbounded check-then-open on the salvage-escape marker read (the posting path) is re-verified at this commit but is NOT re-posted inline: its round-8 thread already occupies that exact location (comment 3879270387, posted as 'R8-10 (2 of 3 locations)'). The blocker remains open in that thread.
[Critical] R8-3 still stands — .github/workflows/qwen-code-pr-review.yml:1790 — the unbounded check-then-open inside read_head_signal (serving both the cede and post-loop moved-to reads) is re-verified at this commit but is NOT re-posted inline: its round-8 thread already occupies that exact location (comment 3879270395, posted as 'R8-10 (3 of 3 locations)'). The blocker remains open in that thread.
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 3 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:reverse audit — stopped at the 5-round cap without converging (round 5 still reported findings)。
未审查:build-and-test — Test (macos-latest, Node 22.x) was skipped in CI (merge_group-gated) and the suite ran locally on Linux only; the macOS-lane failure mode is finding R6-3 itself。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted; the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
收敛姿态下延后(第 9 轮,非阻断)——已记录,本轮不要求修改:共 2 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 9 轮发布了 4 条行内评论,其中 1 条是首次提出;上一轮发布了 6 条(其中 6 条首次提出)。发现反复回到同一批文件:.github/workflows/qwen-code-pr-review.yml(第 4、5、6、8 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)
[Critical] R8-1 still stands — .github/workflows/qwen-code-pr-review.yml:1723 — the unbounded check-then-open on the attempt-start read is re-verified at this commit (code unchanged since round 8; rediscovered this round) but is NOT re-posted inline: its round-8 thread already occupies that exact location (comment 3879270374, posted as 'R8-10 (1 of 3 locations)'). The blocker remains open in that thread.
[Critical] R8-2 still stands — .github/workflows/qwen-code-pr-review.yml:1106 — the unbounded check-then-open on the salvage-escape marker read (the posting path) is re-verified at this commit but is NOT re-posted inline: its round-8 thread already occupies that exact location (comment 3879270387, posted as 'R8-10 (2 of 3 locations)'). The blocker remains open in that thread.
[Critical] R8-3 still stands — .github/workflows/qwen-code-pr-review.yml:1790 — the unbounded check-then-open inside read_head_signal (serving both the cede and post-loop moved-to reads) is re-verified at this commit but is NOT re-posted inline: its round-8 thread already occupies that exact location (comment 3879270395, posted as 'R8-10 (3 of 3 locations)'). The blocker remains open in that thread.
— qwen3.8-max via Qwen Code /review (v0.22.2)
| # through to the review — a missed dedup costs one duplicate | ||
| # review, a false skip would lose one. | ||
| reviewed_shas="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews" --paginate \ | ||
| --arg bot "qwen-code-ci-bot" \ |
There was a problem hiding this comment.
[Critical] R9-1: The head-level dedup this branch added passes --arg bot "qwen-code-ci-bot" to gh api — a flag no gh release has (--arg is a jq flag). The invocation fails at flag-parse time on every run before any request, 2>/dev/null || reviewed_shas="" swallows the error, reviewed_shas is always empty, and the grep -qx "$current_head" skip never fires: the head-level dedup (the round-8 R5-4 fix) is dead code. An away-and-back push (H1→H2→H1) landing inside the watcher's 60s poll gap still yields two full reviews and two posts of the same commit — the multi-hour waste this block exists to prevent, with the fail-open masking total (nothing logs the dead lookup). The replay test stays green only because its gh stub explicitly emulates the nonexistent flag.
Witness (run this round, twice independently):
$ gh api repos/QwenLM/qwen-code --arg bot x --jq '.name' (gh 2.95.0)
unknown flag: --arg — exit 1, before any request
replicated workflow line: reviewed_shas=[] → SKIP never fired → should_review=true
The repo documents this limitation itself (qwen-fleet-shepherd.yml:179 — "gh's --jq accepts a single expression only (no --arg)"; upsert-deferred-issue.sh:253 shows the correct pipe), and this workflow's pre-existing reviews consumer at ~:2331 interpolates the bot login into --jq and works.
Fix — drop --arg and interpolate the constant bot login into --jq (the value is a fixed constant, not event data, so there is no injection concern), or pipe into standalone jq -r --arg bot …:
reviewed_shas="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews" --paginate \
--jq '.[] | select((.user.login // "") == "qwen-code-ci-bot") | (.body // "") | select(contains("<!-- qwen-review-ledger ")) | capture("\"sha\":\"(?<sha>[0-9a-f]{7,64})\"")? | .sha' 2>/dev/null)" || reviewed_shas=""Also make the replay's gh stub reject unknown flags so the fiction cannot be re-emulated.
Fix witness: with a faithful stub (no --arg emulation), re-introducing --arg into the gh api call must turn the should_review=false assertion in 'skips a delayed run whose live head already carries a posted bot review (replayed delay step)' red.
中文说明
[Critical] R9-1:本分支新增的 head 级去重向 gh api 传入了 --arg bot "qwen-code-ci-bot" —— 一个任何 gh 版本都不存在的参数(--arg 是 jq 的参数)。该调用在每次运行时都在参数解析阶段就失败,2>/dev/null || reviewed_shas="" 把错误吞掉,reviewed_shas 永远为空,grep -qx "$current_head" 跳过分支永远不会触发:head 级去重(第 8 轮对 R5-4 的修复)是死代码。落在观察者 60 秒轮询间隙内的「离开又回来」推送(H1→H2→H1)仍会产生对同一 commit 的两次完整评审与两次发布——正是本代码块要消除的数小时浪费,且 fail-open 掩盖是彻底的(没有任何日志记录这次死查询)。回放测试之所以保持绿色,仅因为其 gh stub 显式模拟了这个不存在的参数。
验证证据(本轮两次独立运行):gh api repos/QwenLM/qwen-code --arg bot x --jq '.name'(gh 2.95.0)返回 unknown flag: --arg,退出码 1,未发出任何请求;复刻工作流行后 reviewed_shas=[],跳过从未触发,输出 should_review=true。
仓库自身已两处记录该限制(qwen-fleet-shepherd.yml:179、upsert-deferred-issue.sh:253 的正确管道写法),本工作流 ~:2331 处已有的 reviews 消费者就是把 bot 登录名内插进 --jq 且工作正常。
修复:去掉 --arg,把常量 bot 登录名内插进 --jq(固定常量而非事件数据,无注入顾虑),或改为管道交给独立 jq -r --arg bot …;同时让回放的 gh stub 拒绝未知参数,避免再次模拟这个假象。
修复验证:使用忠实 stub(不模拟 --arg)后,把 --arg 重新加回 gh api 调用,必须使 'skips a delayed run whose live head already carries a posted bot review (replayed delay step)' 中 should_review=false 断言变红。
— qwen3.8-max via Qwen Code /review
— qwen3.8-max via Qwen Code /review (v0.22.2)
| if [ -n "${SUPERSEDE_KILL_DIR:-}" ] && [ -f "${SUPERSEDE_KILL_DIR}/killed" ]; then | ||
| return 0 | ||
| fi |
There was a problem hiding this comment.
[Critical] R4-1: Still stands — re-verified at this commit (code unchanged since round 8; independently rediscovered twice this round). The revert-cede's primary witness — the watcher's kill record — is accepted alone (return 0 with no live-head or timeline corroboration), but SUPERSEDE_KILL_DIR is minted with mktemp -d under the enumerable RUNNER_TEMP with the fixed prefix qwen-review-supersede-kill.*; non-export prevents derivation, not discovery by the same-UID reviewed agent. A prompt-injected agent derives SALVAGE_DIR from the exported QWEN_CI_REVIEW_SALVAGE_OK_FILE, plants ${SALVAGE_DIR}/superseded, enumerates the kill dir and plants killed, then dies non-retryably: the last-look gate finds SUPERSEDE_FILE, the witness short-circuits return 0 before the server-side timeline is consulted, and cede_superseded exits 0 — the review is never posted, the job is green, and no replacement run is queued: silent review suppression, the exact outcome the marker-defense comments say must be impossible.
Fix — accept the local record only when corroborated by the server-side timeline: require at least one HEAD_REF_FORCE_PUSHED_EVENT in the run window whose afterCommit equals EXPECTED_HEAD_SHA. The back-push half exists for a genuine fast-forward-departure + force-push revert (so the real cede shape stays covered) and is absent in the pure-forgery case (no move at all).
Fix witness: a replay (AUTO_REVIEW=true, unmoved stub live head == EXPECTED, empty timeline) whose stub qwen plants SUPERSEDE_FILE and globs RUNNER_TEMP to plant killed must fail red with no Superseded early: — it exits 0 today. The existing 'cedes a killed attempt whose superseding departure was a normal push' replay, whose timeline stub carries the corroborating back-push, must stay green.
中文说明
[Critical] R4-1:依然存在——已在本提交重新验证(自第 8 轮以来代码未变;本轮被两次独立重新发现)。revert 让位的主见证——观察者的 kill 记录——被单独采信(无 live-head 或时间线佐证即 return 0),但 SUPERSEDE_KILL_DIR 是用 mktemp -d 在可枚举的 RUNNER_TEMP 下、以固定前缀 qwen-review-supersede-kill.* 创建的;不导出只能防止推导,不能防止同 UID 的被评审 agent 发现。被提示注入的 agent 从已导出的 QWEN_CI_REVIEW_SALVAGE_OK_FILE 推导 SALVAGE_DIR,放置 ${SALVAGE_DIR}/superseded,枚举 kill 目录并放置 killed,然后以不可重试的方式失败:最后查看门发现 SUPERSEDE_FILE 后,见证在查询服务端时间线之前就短路 return 0,cede_superseded 以 0 退出——评审永不发布、job 显示绿色、也没有排队的接替 run:静默压制评审,正是 marker 防御注释声称必须不可能的结果。
修复:仅在服务端时间线佐证时才采信本地记录——要求运行窗口内至少存在一个 afterCommit 等于 EXPECTED_HEAD_SHA 的 HEAD_REF_FORCE_PUSHED_EVENT。真实的「fast-forward 离开 + 强推回退」形态存在回推事件(真实让位不受影响),纯伪造情形(head 从未移动)则不存在。
修复验证:新增回放(AUTO_REVIEW=true、stub live head 未移动且等于 EXPECTED、时间线为空),其 stub qwen 放置 SUPERSEDE_FILE 并枚举 RUNNER_TEMP 放置 killed,必须红(无 Superseded early:)——现状是 exit 0。现有 'cedes a killed attempt whose superseding departure was a normal push' 回放(其时间线 stub 携带佐证回推事件)必须保持绿色。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| # review, a false skip would lose one. | ||
| reviewed_shas="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews" --paginate \ | ||
| --arg bot "qwen-code-ci-bot" \ | ||
| --jq '.[] | select((.user.login // "") == $bot) | (.body // "") | select(contains("<!-- qwen-review-ledger ")) | capture("\"sha\":\"(?<sha>[0-9a-f]{7,64})\"")? | .sha' 2>/dev/null)" || reviewed_shas="" |
There was a problem hiding this comment.
[Critical] R5-4: Still stands — re-verified at this commit (code unchanged since round 8). This capture extracts the reviewed head from bot review bodies with a FIRST-MATCH of "sha":"…" — but the body is content the reviewed agent authors and posts through a gh guard that never inspects bodies, and the sha is never authenticated. A forged "sha":"<live head>" embedded in a bot-shaped review body makes grep -qx match the live head, so the next queued lifecycle run skips itself — a false suppression of a legitimate review manufactured by content this gate treats as trusted.
Note the interaction with R9-1 above: because gh api --arg fails at flag parse, this dedup currently never executes at all — the two defects must be fixed together. Fixing the flag while leaving the capture unauthenticated ships the forgery live; authenticating while the flag is broken changes nothing.
Fix — authenticate the sha against the review object's own commit context (the reviews API returns commit_id beside the body) instead of parsing body text, and treat the marker as advisory otherwise.
Fix witness: a replay driving the dedup with a review body whose captured sha differs from the review object's commit_id must proceed (should_review=true); removing the authentication while keeping the body parse turns it green on the forged skip.
中文说明
[Critical] R5-4:依然存在——已在本提交重新验证(自第 8 轮以来代码未变)。该 capture 以「首次匹配」方式从 bot 评审正文中提取 "sha":"…" 作为已评审 head——但正文是被评审 agent 自己撰写、并通过从不检查正文的 gh 守卫发布的内容,该 sha 从未经过认证。在 bot 形状的评审正文中嵌入伪造的 "sha":"<live head>" 即可让 grep -qx 匹配 live head,使下一个排队的生命周期 run 自我跳过——由该闸门当作可信输入的内容制造出的合法评审假抑制。
注意与上方 R9-1 的相互作用:由于 gh api --arg 在参数解析阶段即失败,该去重目前根本不会执行——两个缺陷必须一起修复。只修参数而不认证 capture,伪造就会真正生效;只认证而参数仍是坏的,则什么都不会改变。
修复:改用评审对象自身的提交上下文认证 sha(reviews API 在正文旁返回 commit_id),而不是解析正文文本;否则仅把 marker 当作提示。
修复验证:向去重回放传入「正文中捕获的 sha 与评审对象 commit_id 不一致」的评审,必须继续评审(should_review=true);去掉认证而保留正文解析时,伪造跳过会变绿。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| # would otherwise block readFileSync forever (no writer) and | ||
| # kill the watcher permanently. | ||
| if [ ! -f "${SALVAGE_DIR}/compose-seen" ] && [ -f "$COMPOSED_ARTIFACT" ] \ | ||
| && timeout 5 node -e 'const v=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));if(typeof v!=="object"||v===null||Array.isArray(v))process.exit(1);' "$COMPOSED_ARTIFACT" 2>/dev/null; then |
There was a problem hiding this comment.
[Critical] R6-3: Still stands — witnessed again by run this round. The timeout 5 bound the round-8 fix added here introduces a host timeout(1) dependency the runWatcher replay harness does not provide: its stub bin supplies only sleep/gh/pkill/(node) — no timeout stub, although its sibling runScenario has one. On any host without unprefixed coreutils timeout — notably the test_macos lane, which runs this suite — the latch's if condition exits 127 (command not found), the compose latch never fires, and both KEEP assertions fail: expect(valid.marker).toBe('head-a') (:4415) and the planted-FIFO test's latch arm (:4403); 'bounds the compose-artifact reopen against a rename-swapped FIFO' passes vacuously (the latch fails before the node reopen it pins). The lane shows skipped on the PR page (merge_group/schedule-gated; the merge queue is not enabled), so the first red lands on the nightly schedule after merge.
Witness (this round's verifier probe, scratch tree, PATH-shadow timeout exiting 127):
PR code, timeout absent: Tests 2 failed | 9 passed
:4403/:4415 both "expected null to be 'head-a'"
control, real timeout present: Tests 11 passed
Fix — give runWatcher a BOUND-ENFORCING timeout stub (run the child backgrounded with a watchdog kill after the duration argument, mirroring runScenario). The literal shift; exec "$@" shape is insufficient — measured: it turns the rename-swapped-FIFO bound test red, because the swapped-in FIFO then wedges readFileSync forever.
Fix witness: with the stub removed (or on any host lacking timeout(1)), 'latches the compose signal only from a real composed artifact (replayed watcher)' and the latch arm of 'never blocks a signal write or read on a planted FIFO (replayed watcher)' go red; with the bound-enforcing stub, all 11 pass.
中文说明
[Critical] R6-3:依然存在——本轮再次以运行见证。第 8 轮修复在此加入的 timeout 5 上界引入了宿主 timeout(1) 依赖,而 runWatcher 回放 harness 并未提供:其 stub bin 只有 sleep/gh/pkill/(node)——没有 timeout stub,而其姊妹 runScenario 有。在任何没有不带前缀 coreutils timeout 的宿主上——尤其是运行本套件的 test_macos 通道——锁存的 if 条件以 127(command not found)退出,compose 锁存永不触发,两个 KEEP 断言失败:expect(valid.marker).toBe('head-a')(:4415)与放置 FIFO 测试的锁存臂(:4403);'bounds the compose-artifact reopen against a rename-swapped FIFO' 空过(锁存在它所要钉住的 node 重新打开之前就失败了)。该通道在 PR 页面显示为 skipped(merge_group/schedule 门控;合并队列未启用),因此第一次变红发生在合并之后的夜间调度。
验证证据(本轮验证者探针,临时树,PATH 影子 timeout 以 127 退出):无 timeout 的 PR 代码 Tests 2 failed | 9 passed(:4403/:4415 均为 "expected null to be 'head-a'");有真实 timeout 的对照 Tests 11 passed。
修复:给 runWatcher 一个「强制上界」的 timeout stub(子进程后台运行、时长参数到期后由看门狗 kill,与 runScenario 一致)。字面的 shift; exec "$@" 形状不够——实测会把 rename 交换 FIFO 的上界测试变红,因为交换进来的 FIFO 会使 readFileSync 永久阻塞。
修复验证:移除该 stub(或在任何缺少 timeout(1) 的宿主上),'latches the compose signal only from a real composed artifact (replayed watcher)' 与 'never blocks a signal write or read on a planted FIFO (replayed watcher)' 的锁存臂必须变红;加上强制上界的 stub 后 11 项全绿。
— qwen3.8-max via Qwen Code /review (v0.22.2)
…edup (#10123) Address the round-8/9 review Criticals: - The delay-step head-level dedup called `gh api --arg`, a flag gh does not have, so the lookup died at flag parse and the dedup was dead code. Drop the flag and interpolate the constant bot login into the --jq filter. - The dedup parsed the FIRST `"sha":"…"` hit out of model-authored review bodies, a forgeable anchor. Parse only the LAST ledger marker per body (the CLI's parseLedger convention) and authenticate the sha against the review's server-recorded commit_id before skipping; every lookup trouble still fails open. - The revert-cede accepted the watcher's kill record alone, but the record's dir is enumerable under RUNNER_TEMP — non-export stops derivation, not discovery by the same-uid agent. Accept the record only corroborated by a server-side back-push onto the expected head; the timeline pair stays the fallback for a lost record. - The salvage-signal reads gated on [ -f ] and then reopened agent- derivable paths with unbounded cats: a FIFO rename-swapped into the check-then-open window wedged the watcher, the posting path, or the cede forever, and a huge plant could be slurped whole. Each read is now one `timeout 5 head -c N` open; the posting path fails closed. - The replay harnesses supply a bound-enforcing timeout(1) stub (macOS lanes ship none), and the gh stub rejects unknown flags like real gh so the --arg fiction cannot be re-emulated. New replays witness each fix (forged kill record, forged/first markers, commit_id mismatch, rename-swapped FIFOs, huge plants); mutation probes confirmed every witness turns red when its guard is removed. The workflow's .size-baseline ratchet is bumped for the real growth.
|
🤖 Addressed the latest review feedback (round 4/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 4/100 轮)。改动内容与我反驳保留之处如下: Review-round summary — PR #10123Round feedback carried 10 inline Critical findings (rounds 8 and 9 posting Findings and dispositions
Also touched in service of the same fixes:
The review bodies' remaining content is recorded-only this round: the Mutation probes (each witness verified decisive before commit)
Every probe was reverted afterwards and the affected tests re-ran green. One Verification
中文说明评审轮总结 — PR #10123本轮反馈携带 10 条行内 Critical 发现(第 8、9 轮在同一批位置重复发布的五个根因)以及两份评审正文,后者的可执行内容即这些行内线程。全部 10 条行内发现已在提交 c870613 中于代码里解决;本轮没有拒绝、延后或升级任何条目。无冲突( 发现与处置
为同一修复而一并改动的:
评审正文的其余内容本轮仅为记录:第 8/9 轮的延后清单(D7-x、D9-2)明确为「已记录、本轮不要求」, 突变探针(提交前逐一验证每个见证的决定性)
每个探针随后复原,受影响测试重跑为绿。过程中发现并修复了一个探针缺口:最初的 M4 突变(单独采信记录)位于静默时间线守卫之后、不可达,因此伪造回放新增了「无关事件」臂,M4 也改为针对佐证比较本身;包装守卫回放新增了 30 秒 验证
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
8 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- R10-7 af-149-vs-af-151 design-doc pin — already reported as round-7 deferral D7-2 (review 5045544185, re-recorded rounds 8-9)
- R10-8 salvage-note never-fail fallback unwitnessed — already reported as round-9 deferral D9-2 (review 5051639936)
- R10-16 attempt-start forged-epoch clamp — already reported in the round-8 deferral list (review 5049513238)
- R10-17 post-loop salvage outputs missing AUTO_REVIEW gate — already reported as round-7 deferral D7-1 (review 5045544185, re-recorded round 8)
- R10-18 reset/retry rm -rf calls lack || true — already reported as round-7 deferral D7-6 (review 5045544185, re-recorded round 8)
- R10-19 post-loop watcher reap unpinned — already reported as round-6 deferral D6-2 (review 5043372802)
- R10-20 compose-seen latch plantable, bypassing the JSON validation — already reported in the round-8 deferral list (review 5049513238)
- R10-26 report-time hold blind to command-triggered reviews — already reported as round-7 deferral D7-5 (review 5045544185, re-recorded round 8); Critical severity claim rejected this round by verification (outcome is bounded and visible), S…
Not reviewed: reverse audit — stopped at the 5-round cap without converging (round 5 still reported findings).
Not reviewed: build-and-test — Test (macos-latest, Node 22.x) was skipped in CI (merge_group-gated) and the lane did not run locally (suites ran on Linux only).
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted; the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
Deferred under the convergence posture (round 10, not a blocker) — recorded, not requested in this round:
.github/workflows/qwen-code-pr-review.yml:1671 — [probe] initial attempt-start write outside the reset lacks the || true its siblings carry — vanished SALVAGE_DIR aborts the step under errexit.github/workflows/.size-baseline:37 — [review] qwen-autofix.yml baseline entry 432155 is 105 bytes under the actual 432260 at HEADscripts/tests/qwen-pr-review-workflow.test.js:100 — [probe] swapAtOpenStub's unqualified exec cat re-resolves to itself — exec spin loop instead of parked open(2) (twin in qwen-resolve-workflow.test.js)scripts/tests/qwen-pr-review-workflow.test.js:5685 — [review] delay-job dedup's ledger-marker sha capture has no cross-pin to the CLI serializer (fabricated markers)scripts/tests/qwen-autofix-workflow.test.js:9629 — [review] salvage-note marker ↔ fleet-scan exclusion pinned only by fabricated bodies on both sides.github/workflows/qwen-code-pr-review.yml:1925 — [review] salvage-armed cede discards a composed verdict the guard escape would have let post.github/workflows/qwen-autofix.yml:6007 — [review] deferred stale-base branch has no round-cap variant — posts a retry promise that can never fire at MAX_ROUNDSscripts/tests/qwen-pr-review-workflow.test.js:245 — [review] STUB_TIMELINE_STATUS dead switch — gh api failure-exit shape of supersede_reverted_during_run never replayedscripts/tests/qwen-pr-review-workflow.test.js:5298 — [probe] compose-artifact cross-pin covers only the filename half — .qwen/tmp directory half unpinned against REVIEW_TMP_DIRscripts/tests/qwen-pr-review-workflow.test.js:5161 — [probe] SUPERSEDE_KILL_DIR mint prefix unpinned — forged-kill-record replay can go vacuous on a prefix rename
Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)
[Critical] R10-9 — .github/workflows/qwen-code-pr-review.yml:1717 — the compose-latch is the one salvage read that is not size-capped: readFileSync(..., "utf8") slurps the entire agent-writable COMPOSED_ARTIFACT into node's heap; a planted multi-GB invalid-JSON file is re-slurped on every 60s poll for the whole run (probe: 4GB plant → Maximum resident set 4236908 KB per read, exit 1, no latch; fstat-capped fix → 43008 KB; legit artifact still latches). Fix: cap the read via fstat on the already-open fd (reject > ~2 MB). Not posted inline: its resolved anchor (line 1717) overlaps the two existing R6-3 threads at that location (comments 3879270368 / 3881057967) — deterministic overlap drop.
[Critical] R5-6 still stands — .github/workflows/qwen-code-pr-review.yml:1019 — the EXIT trap's rm -rf "$LOG_PATH" clause is poisonable by a permission-resistant plant at the fixed, agent-derivable LOG_PATH; the EACCES failure under errexit aborts the remaining trap clauses, silently skipping the PROXY_BIN, SALVAGE_DIR and SUPERSEDE_KILL_DIR cleanups this same diff added (verified empirically this round: a mid-trap failure under set -e aborts the remaining clauses; unlink requires write permission on the parent directory). Fix: || true on each cleanup clause. NOT re-posted inline: its round-5 thread already occupies this finding (comment 3872716888 — carries no id prefix, so the re-post exemption could not match it). The blocker remains open in that thread.
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 8 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:reverse audit — stopped at the 5-round cap without converging (round 5 still reported findings)。
未审查:build-and-test — Test (macos-latest, Node 22.x) was skipped in CI (merge_group-gated) and the lane did not run locally (suites ran on Linux only)。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted; the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
收敛姿态下延后(第 10 轮,非阻断)——已记录,本轮不要求修改:共 10 条(原文未翻译,列表见上方英文部分)。
机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)
[Critical] R10-9 — .github/workflows/qwen-code-pr-review.yml:1717 — the compose-latch is the one salvage read that is not size-capped: readFileSync(..., "utf8") slurps the entire agent-writable COMPOSED_ARTIFACT into node's heap; a planted multi-GB invalid-JSON file is re-slurped on every 60s poll for the whole run (probe: 4GB plant → Maximum resident set 4236908 KB per read, exit 1, no latch; fstat-capped fix → 43008 KB; legit artifact still latches). Fix: cap the read via fstat on the already-open fd (reject > ~2 MB). Not posted inline: its resolved anchor (line 1717) overlaps the two existing R6-3 threads at that location (comments 3879270368 / 3881057967) — deterministic overlap drop.
[Critical] R5-6 still stands — .github/workflows/qwen-code-pr-review.yml:1019 — the EXIT trap's rm -rf "$LOG_PATH" clause is poisonable by a permission-resistant plant at the fixed, agent-derivable LOG_PATH; the EACCES failure under errexit aborts the remaining trap clauses, silently skipping the PROXY_BIN, SALVAGE_DIR and SUPERSEDE_KILL_DIR cleanups this same diff added (verified empirically this round: a mid-trap failure under set -e aborts the remaining clauses; unlink requires write permission on the parent directory). Fix: || true on each cleanup clause. NOT re-posted inline: its round-5 thread already occupies this finding (comment 3872716888 — carries no id prefix, so the re-post exemption could not match it). The blocker remains open in that thread.
— qwen3.8-max via Qwen Code /review (v0.22.2)
| # back-push onto the expected head during the run — always | ||
| # present when a real move reverted (the return is | ||
| # non-fast-forward), absent in the pure forgery. | ||
| if [ -n "${SUPERSEDE_KILL_DIR:-}" ] && [ -f "${SUPERSEDE_KILL_DIR}/killed" ]; then |
There was a problem hiding this comment.
[Critical] R4-1: Still stands — re-verified at this commit. The revert-cede's kill-record witness chain remains forgeable. SUPERSEDE_KILL_DIR is minted with mktemp -d under the enumerable RUNNER_TEMP with the fixed prefix qwen-review-supersede-kill.* — the code's own comment concedes never-exported stops the agent DERIVING the path, not DISCOVERING it. This round's re-check additionally found the timeline corroboration is itself attacker-satisfiable: the kill-record branch cedes on any server-side event with after == EXPECTED_HEAD_SHA landing at or after START_TS - 15, and the run's own triggering force-push has that exact event shape — the sibling lost-record branch documents exactly this shape as proof-of-nothing ("A lone back-push matches the triggering push of a reopened/ready_for_review run"), while this branch relies on timing alone. A prompt-injected agent (yolo, same uid) plants SUPERSEDE_FILE and the killed record, then dies fatally: with the triggering push's own timeline event as corroboration — whenever reopened/ready_for_review dispatch is fast or the runner clock lags GitHub by ≥15s (these runs skip the 10-minute environment wait) — the run cedes green with no review posted and no replacement run queued (the head never moved): silent suppression of the whole review.
Witness: re-verified in code at this commit by two independent passes this round — the kill-record branch accepts a lone back-push event inside the window that its sibling documents as proof-of-nothing. witness: not run — the trigger turns on GitHub timeline-event timestamps relative to run dispatch on real reopened/ready_for_review runs, which cannot be reconstructed locally.
Require the corroborating event at or after run start, accepting that a skew-shifted real revert degrades to a visible red failure — the same trade the lost-record branch's comment already endorses:
[ "$at" -ge "$START_TS" ] || continueFix witness: a replay with a single back-push timeline event 5s before START_TS plus the planted killed record must fail red instead of ceding; removing the fix turns it green again.
中文说明
[Critical] R4-1:仍然存在——已在当前提交上重新验证。revert-cede 的 kill-record 见证链依然可伪造:SUPERSEDE_KILL_DIR 以固定前缀 qwen-review-supersede-kill.* 在可枚举的 RUNNER_TEMP 下用 mktemp -d 生成——代码自己的注释也承认「不导出」只能阻止 agent「推导」路径,无法阻止「发现」路径。本轮复查还发现其时间线佐证本身可被攻击者满足:kill-record 分支在 after == EXPECTED_HEAD_SHA 且时间不早于 START_TS - 15 的任意服务端事件上即放行 cede,而本 run 自身的触发 force-push 恰好具有完全相同的事件形状——同族的 lost-record 分支明确记载该形状「什么也证明不了」(「单独的回推与 reopened/ready_for_review run 的触发 push 一致」),本分支却只依赖时间窗口。被注入的 agent(yolo、同 uid)放置 SUPERSEDE_FILE 与 killed 记录后让尝试致命失败:只要触发 push 自身的时间线事件能作为佐证——reopened/ready_for_review 分发足够快、或 runner 时钟落后 GitHub ≥15 秒(这类 run 跳过 10 分钟环境等待)——run 就会绿色 cede:不发布评审、也没有接替 run 排队(head 从未移动)——对整个评审的静默压制。
验证证据:本轮两条独立路径在当前提交的代码中重新验证——kill-record 分支接受的单独回推事件正是其同族分支记载为「什么也证明不了」的形状。witness: not run——触发条件依赖真实 reopened/ready_for_review run 的 GitHub 时间线事件时间戳与分发时刻的关系,本地无法重建。
建议要求佐证事件不早于 run 开始([ "$at" -ge "$START_TS" ]),接受时钟偏移下的真实 revert 退化为可见的红色失败——与 lost-record 分支注释已经认可的取舍一致。
修复验证:以单条早于 START_TS 5 秒的回推时间线事件 + 放置的 killed 记录做回放,必须红色失败而非 cede;移除修复后应重新变绿。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| # documents the same limit), so the constant bot login is | ||
| # interpolated. | ||
| reviewed_shas="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews" --paginate \ | ||
| --jq '.[] | select((.user.login // "") == "qwen-code-ci-bot") | ((.body // "") | [match("<!-- qwen-review-ledger [^\n]*"; "g")] | .[-1].string // "") as $marker | (($marker | capture("\"sha\":\"(?<sha>[0-9a-f]{7,64})\"").sha)? // "") as $sha | select(($sha | length) > 0 and $sha == (.commit_id // "")) | $sha' 2>/dev/null)" || reviewed_shas="" |
There was a problem hiding this comment.
[Critical] R5-4: Still stands — strengthened this round. The head-level dedup's trust anchor is fully authorable by the reviewed agent: both compared values — the ledger-marker sha (body text) and the review's commit_id — are caller-supplied on POST /repos/.../pulls/N/reviews. The CLI itself posts reviews with a caller-chosen commit_id (submit.ts assembles { commit_id: payload.commit_id, ... } and validates only presence), so this comment's premise that the equality check anchors to a "server-recorded" commit_id conflates stored-on-server with chosen-by-server — under either conceivable server model (records-as-sent, or coerces-to-post-time-head) the forged review records commit_id == marker sha. The reviewed agent holds CI_BOT_PAT (Run review step env), which posts as qwen-code-ci-bot — exactly the login this filter selects on, and guard_pr_write only blocks when the head moved, so while the head is current the forged review passes straight through (curl with the inherited token bypasses it entirely). Concrete chain: the agent direct-posts a review with commit_id=X and a marker sha=X; the author force-pushes decoy Y then back to X; the synchronize run's delay step sees marker sha X == commit_id X == current_head X → should_review=false → head X is never automatically reviewed again, and the forged review displays to maintainers as the bot's verdict — an attacker-caused, targeted, repeatable false skip.
Witness (probe): the real --jq filter extracted verbatim from this workflow and run with jq — forged bot review {login: qwen-code-ci-bot, body: "…<!-- qwen-review-ledger {\"sha\":\"aaaa…\"} -->", commit_id: aaaa…(40-hex)} → filter returns the sha (grep -qx matches → should_review=false); salvaged shape (marker X, commit_id Y) → ''; third-party login → ''. The commit_id half is traced from packages/cli/src/commands/review/submit.ts (caller-chosen commit_id) and this repo's own documented server model (aone.ts: the Create Review API 422s on a MOVED commit_id — the constraint the forger satisfies). Not run against the live API (no scratch repo designated) — the forgery holds under both server models.
Do not let content the reviewed agent can author decide should_review: corroborate each candidate sha against a source the agent cannot forge (e.g. the workflow's own server-recorded run history — require a successful qwen-code-pr-review run whose headSha equals the candidate before skipping), or drop the content-based skip and let the duplicate run land on the CLI's existing ledger watermark.
Fix witness: a delay-job dedup replay where the matching review exists but no corroborating completed run for that head exists must assert should_review=true; removing the corroboration turns it red.
中文说明
[Critical] R5-4:仍然存在——本轮证据进一步加强。head 级去重的信任锚点可完全由被评审 agent 伪造:比较的两个值——ledger 标记中的 sha(正文文本)与评审的 commit_id——在 POST /repos/.../pulls/N/reviews 上都是调用方提供的。CLI 自身就以调用方选定的 commit_id 发布评审(submit.ts 组装 { commit_id: payload.commit_id, ... },仅校验存在性),因此本处注释中「锚定到服务端记录的 commit_id」的前提把「存储在服务端」与「由服务端选定」混为一谈——在任何一种可能的服务端模型下(按提交值记录,或强制为提交时刻的 head),伪造评审都会记录 commit_id == marker sha。被评审 agent 持有 CI_BOT_PAT(Run review 步骤环境),以 qwen-code-ci-bot 身份发布——正是本过滤器选中的登录名;且 guard_pr_write 仅在 head 移动时才拦截,head 未移动时伪造评审直接通过(用继承的 token 直接 curl 则完全绕过)。具体链条:agent 直接发布一条 commit_id=X、标记 sha=X 的评审;作者强推诱饵 Y 再推回 X;synchronize run 的 delay 步骤看到标记 sha X == commit_id X == current_head X → should_review=false → head X 再也不会被自动评审,而伪造的评审会以机器人结论的形式展示给维护者——攻击者可主动制造、可定向、可重复的误跳过。
验证证据(探针):从本工作流逐字提取真实 --jq 过滤器并用 jq 运行——伪造的机器人评审 → 过滤器返回该 sha(grep -qx 命中 → should_review=false);salvage 形状(标记 X、commit_id Y)→ '';第三方登录名 → ''。commit_id 一环追溯自 packages/cli/src/commands/review/submit.ts(调用方选定 commit_id)与本仓库自己记载的服务端模型(aone.ts:Create Review API 对移动的 commit_id 返回 422——正是伪造者能满足的约束)。未对线上 API 运行(未指定 scratch 仓库)——该伪造在两种服务端模型下均成立。
建议:不要让被评审 agent 可创作的内容决定 should_review——用 agent 无法伪造的来源交叉验证每个候选 sha(例如工作流自身的服务端运行历史:跳过前要求存在一个 headSha 等于候选值的成功 qwen-code-pr-review run),或放弃基于内容的跳过,让重复 run 落在 CLI 既有的 ledger 水位机制上。
修复验证:在 delay-job 去重回放中新增「匹配评审存在、但该 head 没有可佐证的成功 run」的用例,断言 should_review=true;移除佐证后该用例必须变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
|
🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run. 中文说明🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
8 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- fleet-shepherd update-branch sync outside the #10110 hold — already recorded in the round-4 deferral list (review 5037862190)
- stale .size-baseline entries (432155/203437 recorded vs 432260/205370 actual at HEAD) — already reported as the round-10 deferral at .size-baseline:37 (review 5055445698), earlier as R2-9 (comment 3865698450) and R5-7
- af-149-vs-af-151 design-doc pin — already reported as round-7 deferral D7-2 (review 5045544185, re-recorded rounds 8-10)
- swapAtOpenStub exec-cat spin loop (wedge models an exec storm, not a blocked open) — already reported in the round-10 deferral list (review 5055445698)
- watcher TERM→15s→KILL ordering unpinned — already reported in the round-4 deferral list (review 5037862190)
- initial attempt-start write missing the || true its siblings carry — already reported in the round-10 deferral list (review 5055445698)
- armWatcher replays leak enumerable kill dirs into the lane's RUNNER_TEMP — already reported in the round-8 deferral list (review 5049513238)
- report-step hold fail-open fallback arm unpinned — already reported in the round-4 deferral list ('hold tests pin jq filters but not the ROLLUP_R/LIVE_HEAD_R fetches', review 5037862190)
Not reviewed: build-and-test — Test (macos-latest, Node 22.x) was skipped in CI (merge_group-gated) and the suite ran locally on Linux only; the macOS-lane behavior is the subject of finding R11-7 itself.
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted; the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
Deferred under the convergence posture (round 11, not a blocker) — recorded, not requested in this round:
scripts/tests/qwen-resolve-workflow.test.js:66 (+2 locations) — [probe] D11-1 — boundedTimeoutStub inverts GNU timeout(1) zero-duration semantics in both suites — a timeout 0 regression re-opens the unbounded wedge while every replay stays …scripts/tests/qwen-pr-review-workflow.test.js:5090 (+2 locations) — [probe] D11-2 — replay gh stubs serve post-projection data, so the workflow's --jq projections (timeline revert-witness, live_head_moved pr-view) are never executed or pinn…scripts/tests/qwen-pr-review-workflow.test.js:5757 — [probe] D11-3 — delay-step replay gh stub answers the pr branch with a fixed tsv regardless of flags — isDraft guard and the @tsv projection unpinned
Residual risk: this loop is persistently critical — Criticals stood in the previous round's work-list and stand again this round (6 Critical(s)), the rate of first-time findings is not falling (this round 2, previous 0), and the standing Critical backlog is not shrinking. The severity floor will not converge it. Recommendation: land-with-residual-risk — the exit is a maintainer risk-acceptance decision (merge, carrying the residual risk), not another review round. Residual-risk inventory for that decision (maintainer to complete):
| standing Critical | attack surface | attacker-dependency | blast radius |
|---|---|---|---|
| (each standing Critical) | … | … | … |
Advisory only — it does not block this review.
[Critical] R10-1 still stands — .github/workflows/qwen-code-pr-review.yml:1743 — the compose-latch is the one salvage read that is not size-capped: readFileSync(..., "utf8") slurps the entire agent-writable COMPOSED_ARTIFACT into node's heap on every 60s poll; a planted multi-GB invalid-JSON file is re-slurped for the whole run, churning the shared runner, and compose-seen never latches, so a head move before the elapsed threshold CEDE-kills a run whose verdict was already composed. Round-10 probe: 4GB plant → 4236908 KB RSS per read, exit 1, no latch; fstat-capped fix → 43008 KB; legit artifact still latches. Re-rediscovered twice more this round by independent auditors. Fix: cap the read via fstat on the already-open fd (reject > ~2 MB); the latch must stay truth-bound to a parseable JSON object — a torn mid-write fails the parse and latches on the next poll (workflow :1730-1737). Fix witness: a runWatcher arm planting an over-cap composed.json must decide within the harness bound with the latch un-fired; removing the cap turns it red. NOT posted inline: its resolved anchor (line 1743) overlaps the two existing R6-3 threads at that location (comments 3879270368 / 3881057967) — deterministic overlap drop; the blocker remains open here.
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 8 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:build-and-test — Test (macos-latest, Node 22.x) was skipped in CI (merge_group-gated) and the suite ran locally on Linux only; the macOS-lane behavior is the subject of finding R11-7 itself。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted; the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
收敛姿态下延后(第 11 轮,非阻断)——已记录,本轮不要求修改:共 3 条(原文未翻译,列表见上方英文部分)。
残余风险:本循环处于 persistently-critical 形态——上一轮工作清单中的 Critical 本轮依然存在(本轮 6 条 Critical),首次发现的速率没有下降(本轮 2,上一轮 0),且未决 Critical 积压没有减少。severity floor 无法使其收敛。建议:land-with-residual-risk——出口是 maintainer 的风险接受决定(合入并承担残余风险),而非再开一轮评审。供该决定使用的残余风险清单(maintainer 填写):按每条未决 Critical 列出「攻击面 · 攻击者依赖性 · 影响范围」三栏。仅为建议——不阻断本次评审。
[Critical] R10-1 still stands — .github/workflows/qwen-code-pr-review.yml:1743 — the compose-latch is the one salvage read that is not size-capped: readFileSync(..., "utf8") slurps the entire agent-writable COMPOSED_ARTIFACT into node's heap on every 60s poll; a planted multi-GB invalid-JSON file is re-slurped for the whole run, churning the shared runner, and compose-seen never latches, so a head move before the elapsed threshold CEDE-kills a run whose verdict was already composed. Round-10 probe: 4GB plant → 4236908 KB RSS per read, exit 1, no latch; fstat-capped fix → 43008 KB; legit artifact still latches. Re-rediscovered twice more this round by independent auditors. Fix: cap the read via fstat on the already-open fd (reject > ~2 MB); the latch must stay truth-bound to a parseable JSON object — a torn mid-write fails the parse and latches on the next poll (workflow :1730-1737). Fix witness: a runWatcher arm planting an over-cap composed.json must decide within the harness bound with the latch un-fired; removing the cap turns it red. NOT posted inline: its resolved anchor (line 1743) overlaps the two existing R6-3 threads at that location (comments 3879270368 / 3881057967) — deterministic overlap drop; the blocker remains open here.
— qwen3.8-max via Qwen Code /review (v0.22.2)
| # back-push onto the expected head during the run — always | ||
| # present when a real move reverted (the return is | ||
| # non-fast-forward), absent in the pure forgery. | ||
| if [ -n "${SUPERSEDE_KILL_DIR:-}" ] && [ -f "${SUPERSEDE_KILL_DIR}/killed" ]; then |
There was a problem hiding this comment.
[Critical] R4-1: Still stands — re-verified at this commit (code unchanged since round 8). The revert-cede's kill-record witness chain remains forgeable: SUPERSEDE_KILL_DIR is minted with mktemp -d under the enumerable RUNNER_TEMP with the fixed prefix qwen-review-supersede-kill.* — the code's own comment concedes never-exported stops the agent DERIVING the path, not DISCOVERING it (same uid lists the dir and writes). A prompt-injected agent plants ${SUPERSEDE_KILL_DIR}/killed plus SUPERSEDE_FILE, forces a non-retryable death (it controls its own exit), and the step exits 0 green with no review posted and no replacement run queued: silent suppression of the whole review. New this round: the required timeline corroboration is satisfiable without any real head move. An AUTO_REVIEW run triggered by a force-push sets EXPECTED_HEAD_SHA to the triggering push's afterCommit (:1393-1399), and that push's HEAD_REF_FORCE_PUSHED_EVENT carries afterCommit.oid == EXPECTED_HEAD_SHA; if its createdAt lands inside the [START_TS - 15, …) window (dispatch + queue under ~15s on a warm self-hosted runner, widened by the clock skew the -15 tolerance exists to absorb), the forged record's corroboration check passes on the run's own triggering event — the "absent in the pure forgery" invariant the comment states is false for this shape.
Witness:
kill-record branch (workflow :1868-1877), traced at HEAD 44cce38:
a planted record cedes on ANY in-window event with after == EXPECTED_HEAD_SHA;
the triggering push of a force-push-triggered run satisfies both window and match
(the sibling no-record branch explicitly rejects a lone back-push for exactly
this class of event — the kill-record branch has no such protection)
Fix direction: in the kill-record branch only, require the corroborating event to postdate run start — [ "$at" -ge "$START_TS" ], dropping the -15 skew tolerance there. The triggering push necessarily predates START_TS by the full dispatch-plus-setup latency, while a genuine mid-run revert postdates it; the existing cede_revert_ff_kill replay (event at test time) stays green. The branch must keep accepting a LONE back-push with no departure event — "a normal fast-forward departure emits no force-push event" (.github/workflows/qwen-code-pr-review.yml:1859-1861) — so the timing filter is the lever, not a departure/pair requirement. Fix witness: a new arm of refuses a forged kill record when the head never moved (replayed loop) carrying a triggering-event timeline (head-x head-a <now>) must assert red — it cedes green today; cedes a killed attempt whose superseding departure was a normal push must stay green to prove the lone-back-push acceptance survives.
中文说明
[Critical] R4-1:仍然存在——在本次提交上重新验证(代码自第 8 轮起未变)。revert 让位的 kill-record 见证链仍然可伪造:SUPERSEDE_KILL_DIR 以 mktemp -d 在可枚举的 RUNNER_TEMP 下以固定前缀 qwen-review-supersede-kill.* 铸造——代码自己的注释承认「未导出」只阻止 agent 推导路径,不能阻止发现(同 uid 可以列目录并写入)。被提示注入的 agent 放置 ${SUPERSEDE_KILL_DIR}/killed 与 SUPERSEDE_FILE、强制一次不可重试的死亡(它控制自己的退出),步骤即以 0 绿色退出:评审未发布、也没有接替 run 排队——整个评审被静默压制。本轮新增证据:所需的时间线佐证可以在 head 完全未移动时被满足。由 force-push 触发的 AUTO_REVIEW run 会把 EXPECTED_HEAD_SHA 设为触发 push 的 afterCommit(:1393-1399),而该 push 的 HEAD_REF_FORCE_PUSHED_EVENT 恰有 afterCommit.oid == EXPECTED_HEAD_SHA;若其 createdAt 落入 [START_TS - 15, …) 窗口(热自托管 runner 上派发+排队可在 15 秒内,且 -15 容差本就为时钟漂移而设),伪造记录即可凭 run 自己的触发事件通过佐证——注释所称「纯伪造中不存在」的不变量对该形态不成立。
建议:仅在 kill-record 分支要求佐证事件晚于 run 启动——[ "$at" -ge "$START_TS" ](去掉该处的 -15 容差)。触发 push 必然早于 START_TS(完整的派发+准备延迟),而真实的运行中 revert 必然晚于它。该分支必须继续接受「单独回推、无离开事件」的形态(正常 fast-forward 离开不产生 force-push 事件,workflow :1859-1861),因此杠杆是时间过滤而非离开/配对要求。修复验证:为 refuses a forged kill record when the head never moved (replayed loop) 新增携带触发事件时间线的分支,断言红(现状为绿色让位);cedes a killed attempt whose superseding departure was a normal push 必须保持绿色。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| # expression only (no jq argument flags — fleet-shepherd | ||
| # documents the same limit), so the constant bot login is | ||
| # interpolated. | ||
| reviewed_shas="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/reviews" --paginate \ |
There was a problem hiding this comment.
[Critical] R5-4: Still stands — re-verified at this commit (code unchanged since round 8; the round-10 strengthening re-confirmed). The head-level dedup's trust anchor is fully authorable by the reviewed agent: both compared values — the ledger-marker sha (body text) and the review's commit_id — are caller-supplied on POST /repos/.../pulls/N/reviews (the CLI itself posts reviews with a caller-chosen commit_id, packages/cli/src/commands/review/submit.ts:1362), and the gh write guard never inspects review bodies. A forged bot-shaped review embedding <!-- qwen-review-ledger {"sha":"<live head>"} --> and posted with commit_id = the live head manufactures the false skip this block exists to prevent: the queued replacement run — the only run that owns the new head — exits should_review=false, and the head goes unreviewed. The fail-open rationale cuts the wrong way here: a missed dedup costs one duplicate review; a false skip loses a review entirely.
Witness:
dedup filter (workflow :321-322), traced at HEAD 44cce38:
select(($sha | length) > 0 and $sha == (.commit_id // ""))
— both operands caller-supplied on POST reviews; the marker sha is body text
the reviewed agent authors; the gh guard never inspects bodies
Fix direction: dedup on state the reviewed agent cannot author — a CI-owned status check, workflow run record, or other server-recorded field — rather than on review-body text paired with a caller-chosen commit_id. Fix constraint: the salvage shape must keep working — a salvaged historical-head review posts under a NEWER commit_id than its marker sha (workflow :311-315), so any fix must not dedup the live head against a salvaged post. Fix witness: a delay-step replay arm feeding a forged marker+commit_id pair must assert should_review=true; removing the authentication turns it red.
中文说明
[Critical] R5-4:仍然存在——在本次提交上重新验证(代码自第 8 轮起未变;第 10 轮的强化结论复核成立)。head 级去重的信任锚点完全可由被评审 agent 撰写:参与比较的两个值——账本标记中的 sha(正文文本)与评审的 commit_id——都在 POST /repos/.../pulls/N/reviews 时由调用方提供(CLI 自身就以调用方选定的 commit_id 发布评审,packages/cli/src/commands/review/submit.ts:1362),而 gh 写保护从不检查评审正文。一篇伪造的、形似 bot 的评审,内嵌 <!-- qwen-review-ledger {"sha":"<live head>"} --> 且以 commit_id = live head 发布,即可制造本块要防止的误跳过:排队的接替 run(唯一拥有新 head 的 run)以 should_review=false 退出,该 head 从此无人评审。此处 fail-open 的理由方向反了:漏掉一次去重只损失一次重复评审,而一次误跳过会彻底丢失一次评审。
建议:以被评审 agent 无法撰写的状态做去重——CI 自有的 status check、workflow run 记录或其他服务端字段——而非评审正文文本 + 调用方选定的 commit_id。约束:salvage 形态必须继续可用——历史 head 的 salvaged 评审以比其标记 sha 更新的 commit_id 发布(workflow :311-315),修复不得把 live head 与 salvaged 发布去重掉。修复验证:为 delay-step 回放新增喂入伪造 marker+commit_id 对的分支,断言 should_review=true;移除鉴别后该测试变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| # number), so a planted directory there must not skip the reap and | ||
| # leak the watcher — rm -rf absorbs it. The kill itself tolerates an | ||
| # already-reaped watcher (salvage arming exits it on the spot). | ||
| trap '[ -z "${WATCHER_PID:-}" ] || kill "${WATCHER_PID}" 2>/dev/null || true; rm -rf "$LOG_PATH"; [ -z "$PROXY_BIN" ] || rm -rf "$PROXY_BIN"; [ -z "${SALVAGE_DIR:-}" ] || rm -rf "${SALVAGE_DIR}"; [ -z "${SUPERSEDE_KILL_DIR:-}" ] || rm -rf "${SUPERSEDE_KILL_DIR}"' EXIT |
There was a problem hiding this comment.
[Critical] R10-2: Still stands — re-verified at this commit. The EXIT trap's four rm -rf cleanup clauses are poisonable by a permission-resistant plant at fixed, agent-derivable paths: LOG_PATH is ${RUNNER_TEMP}/qwen-review-pr-<n>.jsonl (workflow :980), RUNNER_TEMP survives across jobs on the shared self-hosted runner, and the reviewed agent runs as the same unprivileged runner user and can plant the path as a directory containing a mode-000 child. rm -rf on it fails EACCES and exits 1 — -f does not suppress permission errors on existing operands — and under the step's inherited set -euo pipefail a failing clause inside an EXIT trap aborts the remaining clauses, silently skipping the PROXY_BIN, SALVAGE_DIR and SUPERSEDE_KILL_DIR cleanups this same diff added: every subsequent run of that PR leaks its per-run temp directories into the persistent runner. The kill clause got || true; the four cleanup clauses did not.
Witness:
empirical this round (coreutils 9.4, non-root):
rm -rf on a dir with a non-removable entry -> "Permission denied", exit 1
trap 'false; echo TAIL-RAN' EXIT -> TAIL-RAN never prints
(a failing clause aborts the rest of the trap)
the `true` control prints it
| trap '[ -z "${WATCHER_PID:-}" ] || kill "${WATCHER_PID}" 2>/dev/null || true; rm -rf "$LOG_PATH"; [ -z "$PROXY_BIN" ] || rm -rf "$PROXY_BIN"; [ -z "${SALVAGE_DIR:-}" ] || rm -rf "${SALVAGE_DIR}"; [ -z "${SUPERSEDE_KILL_DIR:-}" ] || rm -rf "${SUPERSEDE_KILL_DIR}"' EXIT | |
| trap '[ -z "${WATCHER_PID:-}" ] || kill "${WATCHER_PID}" 2>/dev/null || true; rm -rf "$LOG_PATH" 2>/dev/null || true; [ -z "$PROXY_BIN" ] || rm -rf "$PROXY_BIN" 2>/dev/null || true; [ -z "${SALVAGE_DIR:-}" ] || rm -rf "${SALVAGE_DIR}" 2>/dev/null || true; [ -z "${SUPERSEDE_KILL_DIR:-}" ] || rm -rf "${SUPERSEDE_KILL_DIR}" 2>/dev/null || true' EXIT |
Fix constraint: the kill must remain the FIRST trap clause — the diff's own comment at .github/workflows/qwen-code-pr-review.yml:1039-1042 states a planted LOG_PATH must not skip the reap and leak the watcher. Fix witness: a replay arm that plants LOG_PATH as a directory with a non-removable child, lets the step exit, and asserts PROXY_BIN/SALVAGE_DIR/SUPERSEDE_KILL_DIR were still removed; it must go red if the || true hardening is dropped from any rm clause.
中文说明
[Critical] R10-2:仍然存在——在本次提交上重新验证。EXIT trap 的四条 rm -rf 清理子句可被「抗权限的放置物」毒化:LOG_PATH 是 ${RUNNER_TEMP}/qwen-review-pr-<n>.jsonl(workflow :980),RUNNER_TEMP 在共享自托管 runner 上跨 job 存留,被评审 agent 以同一非特权用户运行,可把该路径放置为含有 000 权限子项的目录。对其执行 rm -rf 以 EACCES 失败并退出 1(-f 不压制既有操作数上的权限错误),而在步骤继承的 set -euo pipefail 下,EXIT trap 中一条失败的子句会中止其余子句——静默跳过本 diff 新增的 PROXY_BIN、SALVAGE_DIR、SUPERSEDE_KILL_DIR 清理:该 PR 之后每次运行都会向持久 runner 泄漏本次运行的临时目录。kill 子句有 || true,四条清理子句没有。
上方 suggestion:为每条清理子句补 2>/dev/null || true。约束:kill 必须保持为 trap 的第一子句——diff 自己的注释(workflow :1039-1042)要求放置的 LOG_PATH 不得跳过 reap 而泄漏 watcher。修复验证:新增一个把 LOG_PATH 放置为含不可删除子项的目录、让步骤退出、并断言 PROXY_BIN/SALVAGE_DIR/SUPERSEDE_KILL_DIR 仍被清理的回放分支;移除任一 || true 加固后该测试必须变红。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| if (salvageFifo) { | ||
| salvagePath = path.join(tempDir, 'salvage-ok'); | ||
| spawnSync('mkfifo', [salvagePath]); |
There was a problem hiding this comment.
[Critical] R11-7: [certifies-falsely] [new-surface] (1 of 2 locations) The new planted-FIFO wedge tests take a hard dependency on mkfifo(1), which macOS does not ship as a bare command, with no capability probe — and scripts/tests/vitest.config.ts excludes this suite only on win32, so the merge_group/schedule/workflow_dispatch-gated test_macos lane runs it. Here spawnSync('mkfifo', [salvagePath]) returns an ENOENT error object without throwing, so on that lane no FIFO is ever created: the wrapper reads a nonexistent marker, timeout 5 head -c 128 fails instantly, the pin is empty → exit 90, and both arms of bounds the salvage marker read on the posting path (#10110) assert status 90 / 'Blocked PR write' for the wrong reason — the exact unbounding regression this test exists to catch (the R8-10 class) stays green on the macOS lane. The diff shims timeout(1) for exactly this lane ("macOS ships none", :136) yet relies on the equally absent mkfifo(1).
Witness:
macos-15 image manifest (actions/runner-images, fetched live): no mkfifo;
GNU tools ship g-prefixed only
probe over the real extracted wrapper:
A fifo=REAL code=intact: status=90 elapsed=5042ms (bound-kill = the only wedge witness)
B fifo=ABSENT code=intact: status=90 elapsed=35ms (identical assertions — nothing pinned)
D fifo=ABSENT code=REGRESSION(bare cat): status=90 elapsed=9ms — the regression is GREEN
Probe the capability once — matching the suite's own convention (hasGnuMktemp, scripts/tests/qwen-pr-review-workflow.test.js:3445; hasJq) — and gate the FIFO arms with it.skipIf(!hasMkfifo), e.g.:
const hasMkfifo = (() => {
try { execFileSync('mkfifo', ['--help'], { stdio: 'ignore' }); return true; }
catch { return false; }
})();Probe the capability, not process.platform. Fix constraint: the lane facts this rests on are scripts/tests/vitest.config.ts:26-30 (win32-only exclusion) and ci.yml:914-925 (test_macos gated on merge_group/schedule/workflow_dispatch), so the gate must be a capability probe, not a blanket macOS skip. Fix witness: on a host without mkfifo the gated tests must report skipped instead of vacuous green; removing the gate restores the vacuous pass there.
中文说明
[Critical] R11-7:[certifies-falsely] [new-surface](2 处之 1)新增的「放置 FIFO」楔子测试硬依赖 mkfifo(1)——macOS 不提供该裸命令,且此处没有能力探测——而 scripts/tests/vitest.config.ts 仅在 win32 排除本套件,因此 merge_group/schedule/workflow_dispatch 触发的 test_macos 泳道会运行它。这里 spawnSync('mkfifo', [salvagePath]) 以 ENOENT 返回错误对象但不抛异常,该泳道上从未创建出 FIFO:wrapper 读取不存在的 marker,timeout 5 head -c 128 立刻失败,pin 为空 → exit 90,bounds the salvage marker read on the posting path (#10110) 的两个分支以错误的原因断言 status 90 / 'Blocked PR write'——本测试要抓的「解除边界」回归(R8-10 类)在 macOS 泳道上保持绿色。本 diff 恰恰为该泳道 shim 了 timeout(1)("macOS ships none",:136),却依赖同样不存在的 mkfifo(1)。
建议:按套件自身惯例(hasGnuMktemp,scripts/tests/qwen-pr-review-workflow.test.js:3445)做一次能力探测,用 it.skipIf(!hasMkfifo) 门控 FIFO 分支;探测能力而非 process.platform。约束:其依据是 vitest.config.ts:26-30 仅排除 win32、ci.yml:914-925 的 test_macos 触发条件,因此门必须是能力探测而非整段跳过 macOS。修复验证:在无 mkfifo 的主机上,被门控的测试应报告 skipped 而非空洞地变绿;移除门后空洞通过会复现。
— qwen3.8-max via Qwen Code /review (v0.22.2)
| for (const name of Object.keys(plant)) { | ||
| rmSync(join(dir, name), { force: true }); | ||
| execFileSync('mkfifo', [join(dir, name)]); |
There was a problem hiding this comment.
[Critical] R11-7: [certifies-falsely] [new-surface] (2 of 2 locations) Same root, opposite symptom: the pr-review suite's planted-FIFO setups use execFileSync('mkfifo', …), which THROWS on ENOENT with nothing catching it — so on the macOS lane never blocks a signal write or read on a planted FIFO (replayed watcher) (first plant call here), latches the compose signal only from a real composed artifact (replayed watcher) (fifo arm), and writes the salvage outputs only for an armed marker and a real move (replayed block) (the movedToFifo arm at :5523) error red the first time the merge queue or nightly schedule runs them — a lane whose PR-page check reports skipped until then. Additionally the two bounds … rename-swapped FIFO arms pass vacuously on that lane: the stub's inner mkfifo "$last" fails without set -e, no FIFO is ever swapped in, and the wedge those tests claim to pin is never constructed.
Witness:
no-mkfifo PATH model over the unmodified suite:
Tests 3 failed | 2 passed
FAIL never blocks a signal write or read on a planted FIFO
— Error: spawnSync mkfifo ENOENT ❯ runWatcher :4341 (same at :5523)
the two bounds-wedge arms: vacuous GREEN
capability-gate fix: Tests 2 passed | 228 skipped, exit 0
Same fix as the resolve-suite location: one capability probe (hasMkfifo, matching the suite's own hasGnuMktemp convention at scripts/tests/qwen-pr-review-workflow.test.js:3445), then it.skipIf(!hasMkfifo) on the FIFO-dependent tests/arms — probe the capability, not the platform. Fix witness: on a host without mkfifo the gated tests report skipped instead of ENOENT-red; removing the gate restores the red there.
中文说明
[Critical] R11-7:[certifies-falsely] [new-surface](2 处之 2)同一根因、相反症状:pr-review 套件的 FIFO 放置使用 execFileSync('mkfifo', …)——ENOENT 时会抛出且无人捕获——因此在 macOS 泳道上,never blocks a signal write or read on a planted FIFO (replayed watcher)(此处第一次放置调用)、latches the compose signal only from a real composed artifact (replayed watcher)(fifo 分支)与 writes the salvage outputs only for an armed marker and a real move (replayed block)(:5523 的 movedToFifo 分支)会在 merge 队列或夜跑第一次运行时以错误红掉——而该泳道在 PR 页面此前一直显示 skipped。此外两个 bounds … rename-swapped FIFO 分支在该泳道空洞通过:stub 内部的 mkfifo "$last" 在无 set -e 下失败,从未换入 FIFO,测试声称钉住的楔子从未被构造。
修复同 resolve 套件处:一次能力探测(hasMkfifo,沿用套件自身 hasGnuMktemp 惯例,:3445),再对依赖 FIFO 的测试/分支 it.skipIf(!hasMkfifo)——探测能力而非平台。修复验证:在无 mkfifo 的主机上,被门控的测试报告 skipped 而非 ENOENT 红;移除门后红色复现。
— qwen3.8-max via Qwen Code /review (v0.22.2)
…nticate the cede/skip witnesses (#10123)
|
🤖 Addressed the latest review feedback (round 5/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 5/100 轮)。改动内容与我反驳保留之处如下: Autofix round summary — PR #10123One commit ( Feedback points and decisionsR10-9 / R10-1 — compose-latch read is the one unbounded salvage read — FIXED(reviews rv:5055445698 / rv:5056587741;
R5-6 / R10-2 — EXIT trap cleanup clauses poisonable — FIXED(inline rc:3885344162;
R4-1 — revert-cede kill-record corroboration forgeable via the triggering push — FIXED(inline rc:3884356921, rc:3885344144; kill-record branch of
R5-4 — head-level dedup trust anchor fully authorable by the reviewed agent — FIXED(inline rc:3884356928, rc:3885344156; delay job
R11-7 (2 locations) — planted-FIFO wedge tests hard-depend on
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not reviewed: reverse audit — stopped at the 5-round cap without converging (round 5 still reported findings).
Not reviewed: build-and-test — Test (macos-latest, Node 22.x) was skipped in CI (merge_group-gated) and the changed workflow test suites ran on the Linux lane only.
Not explored to full depth (tool budget reached): chunk 5: executing scripts/tests/qwen-pr-review-workflow.test.js to confirm green (the review worktree has no node_modules; npm ci plus the prepare build of all wo….
Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted.
Deferred under the convergence posture (round 12, not a blocker) — recorded, not requested in this round; 1 Critical(s) among them are deferred by their axes — fails-closed on new surface, where no wrong result is certified and the merge base had neither the surface nor the defect — and remain follow-up work recorded in the findings artifact:
.github/workflows/qwen-code-pr-review.yml:1705 — [probe] Critical [fails-closed] [new-surface] D12-1 — the two new salvage-state rm -rf cleanups (reset :1705, retry :1999) lack the || true guard every EXIT-trap cleanup carries — a permissio….github/workflows/qwen-code-pr-review.yml:331 — [probe] D12-2 — the new head-level-dedup corroboration reads one 30-item page of the repo-wide actions/runs endpoint — this workflow's own successful run falls off page 1 and the dedup silentl….github/workflows/qwen-code-pr-review.yml:333 — [probe] D12-5 — dedup corroboration counts ANY successful run of this workflow, but cede/delay-skip exits 0 create successful runs that reviewed nothing — a forged marker+commit_id pair plus s….github/workflows/qwen-code-pr-review.yml:2038 — [probe] D12-3 — the post-loop salvage-output block is the only forgeable-marker consumer missing the AUTO_REVIEW gate its four sibling sites carry — an explicit run with a forged marker and a….github/workflows/qwen-code-pr-review.yml:2047 — [probe] D12-6 — salvage_moved_to is emitted from the one-shot watcher's first-move snapshot and never reconciled against the live head — the historical-head note names a destination/anchor th….github/workflows/qwen-code-pr-review.yml:2119 — [probe] D12-4 — the historical-head note step's gh pr comment posting call has no test pin or replay — dropping it keeps the whole suite green while salvaged runs silently stop publishing the…scripts/tests/qwen-pr-review-workflow.test.js:47 (+1 locations) — [probe] D12-7 — hasMkfifo probes mkfifo --help (a GNU-ism) rather than FIFO creation — macOS ships BSD mkfifo that rejects --help, so all eight FIFO-wedge tests silently skip…scripts/tests/qwen-resolve-workflow.test.js:80 (+1 locations) — [probe] D12-8 — boundedTimeoutStub inverts GNU timeout(1) zero-duration semantics (timeout 0 disables the bound; the stub kills at once) — a timeout-0 regression re-opens the u…scripts/tests/qwen-pr-review-workflow.test.js:113 (+1 locations) — [probe] D12-9 — swapAtOpenStub never blocks on the FIFO open its comment claims — installed as both head and cat, its trailing exec cat re-resolves to itself, spinning an en…
中文说明
未审查:reverse audit — stopped at the 5-round cap without converging (round 5 still reported findings)。
未审查:build-and-test — Test (macos-latest, Node 22.x) was skipped in CI (merge_group-gated) and the changed workflow test suites ran on the Linux lane only。
未探索到全部深度(达到工具调用预算):chunk 5:executing scripts/tests/qwen-pr-review-workflow.test.js to confirm green (the review worktree has no node_modules; npm ci plus the prepare build of all wo…。
未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-code-pr-review.yml: actionlint embedded-shell source mapping is not yet supported — not linted。
收敛姿态下延后(第 12 轮,非阻断)——已记录,本轮不要求修改;其中 1 条 Critical 按其失败方向与对照基线延后——fails-closed 且 new-surface:未认证任何错误结果,且 merge base 既无该功能面也无该缺陷——作为后续工作记录在 findings 工件中:共 9 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max@e6bf8ffe via Qwen Code /review (v0.22.3)
Compose the report-step stale-base guards resolved in conflict: main's convergence-break park guard (#10122, af-154) wraps this branch's review-in-flight hold (#10110), which moves its rationale anchor to af-155 after main's convergence set took af-151..af-154. A parked or unreadable park state now skips the merge before the live-review probe ever runs; the probe's deferral path is unchanged otherwise. The .size-baseline ratchet records the merged workflow sizes.
|
🤖 Addressed the latest review feedback (round 6/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 6/100 轮)。改动内容与我反驳保留之处如下: AutoFix round summary (verification-repair pass) — PR #10123This is the same-run verification-repair pass after the deterministic gate rejected the round's merge commit with 1. Deterministic rejection (lint) — root cause found, fixedEvery one of the 4082 lint errors sits in exactly 7 files, all under Fix applied: removed the two gitignored residue directories that attempt left behind ( 2. Self-correction: the workflow tests are green under their own configEarly in this pass, ad-hoc runs of the workflow test files via 3. Unrelated flake observed in the full suite (recorded, not touched)
4. Failed check:
|
What this PR does
Fixes #10110 — both halves.
Salvage instead of cancel (review workflow).
cancel-in-progressno longer fires onsynchronize(only onclosed), so a push no longer discards the in-flight review run declaratively. A synchronize run now queues PENDING in the PR-scoped concurrency group (the latest push replaces the pending slot) while the in-flight run's new supersede watcher polls the live head and decides for itself: before the salvage threshold it ends the attempt cleanly (writes a supersede marker, kills the review tree, exits 0) so the queued replacement starts within a poll interval; past the threshold it finishes and posts against the head it reviewed, as a historical-head review. The threshold is "compose artifact present" (the composed-verdict file the review skill writes — findings final, posting minutes out) OR elapsed ≥ 50% of the size-aware review budget (tunable via a newQWEN_REVIEW_SALVAGE_ELAPSED_PERCENTrepo variable; 50% of a budget tier approximates the issue's "80% of typical wall time" since reviews measure ~55–70% of their tier). The gh-wrapper write guard gains a salvage escape pinned to the reviewed head's SHA — the CLI's existing presubmit head-drift gate (classifyHeadDrift) already annotates the review body and caps the verdict for the drift — and a new step posts a bilingual "historical-head review" note naming the anchor the next incremental review starts from. The delay job also skips a queued run whose event head went stale while it waited, before review-pr spends runner setup on it.Extend the hold to the loop's own report-time push (autofix workflow). The scan's dispatch gate (#8888/#8899) already holds rounds while review-pr is live, but the report step's stale-base retry calls
update-branchhours after that gate last looked — the one loop-owned head move outside the hold. It now runs the same probe pair as the scan gate first (statusCheckRollup filter + runs-API fallback for delay-parked lifecycle runs, fail-open on probe errors so stale-base recovery can never wedge on a transient API error) and, on a live review, defers the refresh with the retry branch's exact sentinel semantics: feedback stays live, the next round re-runs, and its report step performs the refresh once the review has landed. The deferred round joins the consecutive-failure streak-reset needles and the current-round breaker exemption, mirroring the retry branch. Rationale recorded asqwen-autofix.md#af-148.Why it's needed
On PR #9729, run 32726618419 was cancelled by a push 4h06m in — minutes from posting — and cancelled review runs wasted ~7h41m of runner time across that one PR's 3.2 days. The cancelling push at 16:16:50 was a maintainer's own push, which no hold can cover: only salvage recovers that class. The loop's report-time
update-branchis the remaining bot-initiated push the #8899 hold missed; even for reviews a synchronize cannot cancel (comment-triggered runs use per-run groups), a head move still bricks their posting through the head-pinned write guard, so the hold protects those too.Reviewer Test Plan
How to verify
npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-pr-review-workflow.test.js scripts/tests/qwen-resolve-workflow.test.js scripts/tests/qwen-autofix-workflow.test.js scripts/tests/workflow-size.test.jssalvage_eligibleis extracted from the workflow and driven over the PR feat(serve): backfill session PR bindings and refresh their merge state #9729 numbers (4h06m on a 360-minute budget salvages; 90m in cedes); the retry loop is replayed with the supersede/salvage marker files present (superseded attempt ends clean without retrying, salvage-armed failed attempt cedes instead of re-reviewing a superseded head, salvage-armed success completes); the gh wrapper is executed with a salvage marker (matching pin posts, wrong pin still blocks with exit 90, closed PR still blocks); the report-step rollup and runs-API jq probes run over live/completed/foreign-check fixtures.composedNameForinpackages/cli/src/commands/review/run.ts, thecancel-in-progressexpression is pinned verbatim (closed only), and the CONSEC_FAIL guard regex includes the newSTALE_BASE_DEFERREDclause.bash -npasses on every modified run block (extracted via YAML parse);.github/scripts/check-workflow-size.shstays green (qwen-autofix.yml has ~35 KB of headroom).PR #8894health-probe tests, one env-forge lock test) fail identically onmainand are unrelated.Evidence (Before & After)
N/A (CI workflow behavior; evidence is the replayed-bash test suites above and the run-time analysis of PR #9729 in #10110).
Tested on
中文说明
过阈值后落地而非取消(review workflow):
cancel-in-progress仅在closed时触发,synchronize不再声明式取消在飞 review。push 产生的新 run 在 PR 级并发组中排队等待(最新 push 顶替排队位),在飞 run 内新增 supersede watcher 轮询 live head 自行决定:未过 salvage 阈值则干净结束本次尝试(写 supersede 标记、终止 review 进程树、exit 0),排队的接替 run 在一个轮询间隔内启动;已过阈值(compose 工件已产出,或已消耗按尺寸分级预算的 50%,可用QWEN_REVIEW_SALVAGE_ELAPSED_PERCENT仓库变量调整)则跑完并针对其实际评审的 head 发布为历史 head 评审。gh wrapper 写保护增加与被评审 head SHA 绑定的 salvage 逃生口;CLI 已有的 presubmit head-drift 门会标注并封顶 verdict;新增步骤发布双语「历史 head 评审」说明,指出下一次增量评审的锚点。delay job 也会在排队期间 head 已过时时提前跳过。把 hold 扩展到循环自己的报告期 push(autofix workflow):report 步骤的 stale-base
update-branch是 #8899 dispatch hold 之外唯一的循环自有 head 移动。现在它先用与 scan gate 相同的探测对(rollup 过滤 + runs-API 兜底,探测出错时 fail-open)检查是否有在飞 review;有则以与 retry 分支完全相同的 sentinel 语义推迟刷新(反馈保持有效,下一轮的 report 步骤在评审落地后完成刷新)。deferred 轮次同样加入连续失败断路器的豁免。设计记录见qwen-autofix.md#af-148。动机:PR #9729 上 run 32726618419 被一次 push 在 4h06m 时取消——距发布只差几分钟——该 PR 3.2 天内被取消的评审 run 合计浪费约 7h41m。16:16:50 的取消源是维护者本人的 push,任何 hold 都无法覆盖,只有 salvage 能挽回这一类;而循环报告期的
update-branch是 #8899 hold 漏掉的最后一个 bot 发起 push。Rebase note (2026-08-26): rebased onto current
mainafter the design-record migration (#9677) landed — the rationale anchor moved fromaf-074toaf-148, the scan's delay-window entryaf-099was updated for the new supersede semantics, and both workflows'.github/workflows/.size-baselineentries are bumped per the growth ratchet (#9931): review workflow +15 KB (watcher/salvage machinery), autofix +6 KB (probe pair) — real feature growth, long-form rationale kept in the design record.